Your LLM bill, minus the calls you don’t need.
Cut what you spend on every classification — without giving up accuracy. Postrule moves each one off the LLM onto a fast in-process model, but only after it’s proven just as accurate on your own data. Your original rule stays as the safety net.
pip install postrule
Python ships in v1.0. TypeScript, Go, Rust, and Java clients are v1.1 follow-ons, alongside a WASM build of the gate primitive — so every client will share one audit-chain format and one statistical contract.
Already audited the eight LLM-broker libraries enterprise teams build on:
LangChain, LlamaIndex, Haystack, AutoGen, CrewAI, DSPy, LiteLLM, Instructor.
13,841 files scanned, 1,369 classification sites surfaced.
See them ranked by the LLM bill they’d retire →
To re-derive on any of them,
pip install postrule && postrule analyze /path/to/repo.
Stop paying frontier prices for classifications a model can learn once. Postrule is the efficiency layer for everything your code asks an LLM to do.
Real analyzer output, real codebases
See the classification sites Postrule finds in real OSS Python repos. And the LLM bill they'd retire.
Pick a repo. Drag the sliders. Watch the savings move. Every row below comes from the v1.0 analyzer running on the real source.
v1.1 Run live on any public GitHub repo
Live custom-repo analysis ships in v1.1 (Pyodide-in-browser;
the analyzer fetches the repo from GitHub and runs in
your tab — your code never touches our server). For now,
pip install postrule && postrule analyze .
runs the same scan on your machine in a few seconds.
Loading marimo…
| file:line | function | pattern? | labels? | regime? | priority? |
|---|---|---|---|---|---|
| Picking a preset… | |||||
If this repo's LLM bill looked like…
Sliders are your inputs; the savings number is what graduating to in-process ML heads recovers, weighted by how many of the detected sites are realistically high-priority (score ≥ 2.0 on the composite).
Click a provider to set the slider to a typical classifier-call cost at that rate.
Want to run this on your private repo? Same scan, same JSON, on your own machine in under a minute.
See your savings on your own code
Paste a Python file. See its priority table.
Drop a function or a whole file (under 200 KB) below. Postrule's
static analyzer runs entirely in your browser via Pyodide —
your code never leaves this tab. Detected
classification sites get the same priority + volume + lift
scoring you'd see from postrule analyze locally.
Results render here — function name, pattern, labels, regime, volume, priority, lift status, and the matched source range. Same shape as the local CLI output.
Save this analysis or send it to a teammate.
We'll email a Markdown copy with the postrule init
commands for each top site. Your snippet is not stored
— only the email + the result counts.
From install to your first savings
Install Postrule and see your first classification site, locally.
The walkthrough below is the same path you'd run in your terminal right now. Steps 4 and 7 are optional (auto-lift and the MCP server); the other five are the core install path. Every command is copy-able. Every output snippet is from the v1.1 CLI, not a mockup.
-
1
Install
No GPU, no torch, no service deps. Sklearn-class only.
pip install postruleSuccessfully installed postrule-1.3.0~30s on a warm pip cache, up to a minute cold.
uv pip install postruleworks too if you've moved on. -
2
Find your classification sites
Pure-Python AST scan. No network, no upload — your code stays on your machine.
postrule analyze .Postrule static analyzer — classification sites ============================================================ Root: /Users/you/myapp Files scanned: 84 Sites found: 7 file:line function ptn labels regime vol priority ----------------------------------------------------------------------------------- src/triage.py:14 triage_ticket P1 3 narrow hot 3.50 src/router.py:88 route_intent P1 7 narrow warm 2.10 src/moderation.py:31 classify_post P1 3 narrow warm 1.05 … By regime: narrow: 5 high_card: 1 unknown: 1 Next step: `postrule init <file>:<function> --author @you:team` to wrap the highest-priority site. Estimated LLM spend retired: $1,840–$38,200/yr across 7 sites (token cost at counterfactual volume).Pure-Python AST — seconds on a big repo. Add
--jsonfor the machine-readable shape the website demo uses, or--project-savings --format markdownfor the full savings breakdown. -
3
Preview the wrapper
See the AST-injected diff before you commit a single line.
postrule init src/triage.py:triage_ticket --author @you:team --dry-run# would modify src/triage.py: 3 labels (inferred), phase=RULE --- src/triage.py (before postrule init) +++ src/triage.py (after postrule init) @@ -1,3 +1,10 @@ +from postrule import ml_switch, Phase, SwitchConfig + +@ml_switch( + labels=['auto_close', 'escalate', 'queue'], + author='@you:team', + config=SwitchConfig(phase=Phase.RULE), +) def triage_ticket(ticket: dict) -> str: title = (ticket.get("title") or "").lower() # would write test_triage_ticket.py: import pytest from triage import triage_ticket # adjust the import path to your project KNOWN_LABELS = {'auto_close', 'escalate', 'queue'} SAMPLES = [ # ((ticket,), 'expected_label'), ] @pytest.mark.parametrize("args, expected", SAMPLES) def test_triage_ticket_preserves_behavior(args, expected): assert triage_ticket(*args) == expectedTwo artifacts, both previewed: the decorator diff and a preserved-behavior test — the Phase-0 gate that fails if the switch ever diverges from today's output. Drop
--dry-runto write them in place, or add--no-testto skip the test. -
4
Auto-lift the wrapper (optional)
Extract per-branch handlers and hidden-state evidence into a Switch subclass, so the LLM/ML head can graduate.
postrule init src/triage.py:triage_ticket --author @you:team --auto-lift# would modify src/triage.py and create __postrule_generated__/triage__triage_ticket.py --- src/triage.py (before) +++ src/triage.py (after) @@ -1,3 +1,5 @@ +from __postrule_generated__.triage__triage_ticket import TriageTicketSwitch + def triage_ticket(ticket: dict) -> str: - # original body unchanged - ... + return TriageTicketSwitch().dispatch(ticket).label +++ __postrule_generated__/triage__triage_ticket.py (new) @@ -0,0 +1,18 @@ +from postrule import Switch + +class TriageTicketSwitch(Switch): + def _evidence_title(self, ticket: dict) -> str: + return (ticket.get("title") or "").lower() + + def _rule(self, evidence) -> str: + if "crash" in evidence.title or "outage" in evidence.title: + return "escalate" + if evidence.title.endswith("?"): + return "queue" + return "auto_close" + + def _on_escalate(self, ticket): ... # extracted side effect + def _on_queue(self, ticket): ... # extracted side effect + def _on_auto_close(self, ticket): ... # extracted side effect--auto-liftadds the per-branch (_on_*) and per-evidence (_evidence_*) machinery the analyzer would otherwise flag as missing. Skip it on simple sites; reach for it once a site grows hidden state or branch-specific side effects. -
5
Run your code as usual
No command. The wrapper does the work.
Outcomes log silently to
runtime/postrule/<switch>/every time your app callstriage_ticket(...). Phase 0: the rule still decides; the switch just records. Hand the same record IDs toswitch.record_verdict(record_id, Verdict.CORRECT)when downstream signals (CSAT, resolution code, human review) tell you whether the classification was right. -
6
See your ROI
Self-measured from your own outcome logs. Knobs are exposed for your own assumptions.
postrule roi runtime/postrule/Postrule self-measured ROI report ============================================================ Switches tracked: 1 Total outcomes: 4,210 model calls avoided: 4,210 Disk usage: 128.4 KB switch outcomes acc eng+ttm+reg (USD) tokens (USD) -------------------------------------------------------------------------------------- triage_ticket 4,210 92% $12,000–$38,000 $2,400–$7,600 -------------------------------------------------------------------------------------- Portfolio savings range: $14,400 – $45,600 / year of which token savings: $2,400 – $7,600 Assumptions (adjust via ROIAssumptions): eng_cost_per_week=$4,000 baseline_weeks=1.6-3.5Self-measured from your own outcome logs. Override the assumption bands with
--engineer-cost-per-week/--monthly-value-low/--monthly-value-high. -
7
Drive Postrule from your IDE (optional)
Postrule ships an MCP server (stdio) so Claude Code, Cursor, and any MCP-aware client can drive the CLI directly.
pip install 'postrule[mcp]' postrule mcp// ~/.config/claude-code/mcp.json (or your client's equivalent) { "mcpServers": { "postrule": { "command": "postrule", "args": ["mcp"] } } }Every CLI verb (
analyze,init,roi,benchmark,report,graduate,verdict, …) is reachable as an MCP tool. Your assistant callspostrule initandpostrule benchmarkfor you, with deterministic output.
That's it. The same path runs on every classifier in your codebase.
Already running it locally? Run postrule login to save your
analyses, share them with a team, and pull cohort tuned-defaults.
Free account, GitHub login, no card.
The launch artifact
The report card is the audit trail.
Every wrapped switch produces a markdown report card — what the gate saw, when it fired, the cost trajectory, the drift posture. Same artifact for compliance, your CFO, and your on-call engineer. No separate dashboard. No SaaS lock-in for the evidence.
Three commands, three cards. The discovery card from
postrule analyze --report shows you what's
wrappable on day 0. The per-switch card from
postrule report <switch> ships at
graduation. The project rollup from
postrule report --summary is the cockpit
view across everything you've wrapped.
$ postrule report triage_rule # Triage Rule — Graduation Report Card Generated 2026-04-29 22:15 UTC. Site: src/triage.py:triage_rule. Fingerprint: a1b2c3d4ef567890. ## Status Phase: ML_PRIMARY — graduated 2026-04-25 at outcome 312. Gate (McNemarGate, α = 0.01) fired with p = 4.2 × 10⁻⁴. Effect size: rule 78.4% → ML 87.2% (+8.8 pp). ## Cost trajectory Pre-grad Post-grad Reduction Per call $0.0042 $0.000003 99.93% Per 1M calls $4,200 $3.00 $4,197 saved Latency p50 412 ms 0.8 ms 99.81% ## Drift posture Green. Last check 2026-05-01 03:00 UTC. Re-evaluation triggers if accuracy drops >2pp over a 7-day window.
Wrap your if/else. Walk away. Come back to a
classifier paid in microseconds.
from postrule import ml_switch
from myapp.support import auto_close, queue_for_human, escalate_to_oncall
# Each label is paired with a downstream action.
# The decorator wires classification, dispatch, and outcome
# logging at the call site. The body is the exact if/else
# your team would have inlined anyway.
@ml_switch(labels={
"auto_close": auto_close,
"queue": queue_for_human,
"escalate": escalate_to_oncall,
})
def triage_ticket(ticket: dict) -> str:
title = (ticket.get("title") or "").lower()
if "crash" in title or "outage" in title:
return "escalate"
if title.endswith("?"):
return "queue"
return "auto_close"
triage_ticket(ticket) # classifies AND fires the matching handler
Zero behavior change on day one. Your team ships the exact
if/else they would have inlined
anyway. Postrule does the work that usually takes a six-month
ML migration project: outcome logging, training, shadow
evaluation, graduation. By month six, that same
triage_ticket(ticket) call is paid in
microseconds and fractions of a cent. Nobody touches the
call site.
What you actually ship
An LLM bill that retires itself
Most production classifications eventually accumulate enough evidence to retire the LLM tier. Postrule is the substrate that earns it — automatically, with a statistical floor under every promotion. By month six you're paying microseconds and pennies, not seconds and dollars, for the same call.
Stop hand-coding classifiers
The if/else ladders, regex routers, and try/except heuristics you write because an LLM was too expensive to call there — you don’t need them. Describe what you want; Postrule starts with a model and graduates it to something cheaper and faster than the code you’d have written. The call site is permanent — only the brain underneath moves.
A rule still behind everything
Your hand-written rule is preserved structurally at every phase. When ML is uncertain, falls through to LLM. When LLM is uncertain, falls through to rule. Safety-critical sites cap at "ML-with-fallback" — no ML-primary for authorization, ever, by construction.
How it works
The mechanism, for the curious.
None of what follows is required reading to use Postrule —
pip install postrule and the decorator above
are sufficient. The rest of this page is for the engineer
asking "yes, but how do you actually know when to
graduate?"
Six phases. Three evidence-gated graduations. One rule floor.
Each phase routes decisions the same way every time. The lifecycle only graduates to the next phase once enough outcome evidence has accumulated to prove the upgrade is real, not a coincidence. The bar is conservative by default and the math is in the paper for those who want to verify it.
-
RULEYour function decides.
-
MODEL_SHADOWRule decides; LLM watches.
-
MODEL_PRIMARYLLM decides; rule is fallback.
-
ML_SHADOWML trains from outcomes.
-
ML_WITH_FALLBACKML decides; on uncertainty falls through LLM, then rule.
-
ML_PRIMARYML decides; circuit breaker reverts to rule on anomaly. safety_critical=True refuses this phase at construction.
Predecessor cascade. Each phase's low-confidence fallback is its predecessor's full routing. Phase 4 → Phase 3 → Phase 2 → Rule, in the order each tier was earned. Promotions add tiers; uncertainty walks them back down. The rule fires when every learnable tier above it is below threshold.
Drift detection rides along. The same
evidence test runs in reverse: if accumulated outcomes
show the rule has reclaimed the lead, the lifecycle
demotes by one phase. Safety-critical sites cap at
ML_WITH_FALLBACK — no ML-primary for
authorization decisions, ever, by construction.
19 text datasets, six domains. The right tier wins per site — and learning when to graduate beats always going to ML.
Across 19 public text datasets in six domains — sentiment, intent, moderation, emotion, topic, and spam — graduated autonomy (letting each site earn its way to ML) beat an always-graduate-to-ML baseline by +7.5 pp accuracy (95% CI 2.4–12.7) while using 34% fewer labeled examples. On about half the streams the LLM stayed the best tier and the system correctly kept it there — the point isn't to push everything to ML, it's to land each site on the tier that wins. The eight transition curves above are representative: the rule's flat accuracy against the ML head's rise as outcomes accumulate, with the dashed line marking where the evidence gate cleared (p < 0.01).
Three regimes by cardinality × rule-keyword affinity
Regime I — rule near optimum
codelangs (12 langs, 87.8% rule).
Rigid syntax keywords (def,
function, subroutine) leave
the rule within ~10 points of ML. Graduation is
evidence-justifiable but the lift is modest; the
lifecycle's value is audit chain and drift symmetry.
Regime II — rule usable
ATIS (70.0% rule), TREC-6 (43.0% rule). Mid-cardinality with strong-to-moderate keyword affinity. The rule ships on day one. ML decisively wins (88.7%, 85.2%) and the gate clears at the first 250-outcome checkpoint.
Regime III — rule at floor
HWU64, Banking77, CLINC150 by high cardinality; Snips, AG News by weak keyword affinity. Rule is at-or-near chance on day one. Postrule's role here is cold-start substrate — outcome logging while a zero-shot LLM runs in front, trained head warms up underneath.
The mechanism is modality-agnostic by design. The gate operates on right/wrong outcome streams that any classifier produces — text, image, audio, or structured data. Preliminary image and audio results (CIFAR-10, ESC-50) are in the paper; native audio and video models are future work.
postrule bench →
The full study is under peer review.
The same evidence that lifts accuracy retires the LLM bill.
When the lifecycle reaches its final phase, every classification has earned two things at once: the right to trust the ML head's accuracy, and the right to skip the LLM tier permanently. Latency drops from hundreds of milliseconds to sub-millisecond. Per-call cost drops to essentially zero. The math, illustrative not predictive, at three scales:
| Workload | LLM tier (~$0.005/call) | ML head (post-graduation) | Annualized swing |
|---|---|---|---|
| 10⁴/day · small SaaS | ~$50/day | ~$0/day | ~$18k/year |
| 10⁶/day · mid-scale | ~$5,000/day | ~$0/day | ~$1.8M/year |
| 10⁸/day · large platform | ~$500,000/day | ~$0/day | ~$182M/year |
Linear in unit cost. A frontier-tier model at $0.05/call multiplies the savings 10×; a small-tier model at $0.0005 divides them 10×. The shape that matters is constant: once the lifecycle has graduated, the ML-head tier is essentially free per call. Workloads that started on an LLM because their rule was a non-starter see the most visible economic effect.
Find the classifiers in your codebase. Free. 30 seconds.
postrule analyze ./my-repo
Runs entirely locally. No upload. No signup. Walks your Python source, identifies classification decision points via six AST patterns, scores each for wrap priority, and outputs a JSON artifact for CI diff tracking.
$ postrule analyze ./my-repo Scanned 12,408 Python files; found 7 classification sites. src/support/triage.py:42 — 5 labels, medium cardinality Volume: hot · Lift: auto-liftable · Priority: 4.50/5 Regime: narrow-domain rule-viable (ATIS-like) Estimated: rule accuracy ~70%; ML would add ~15-20pp after ~500 outcomes src/mod/content_score.py:88 — 3 labels, binary-ish Volume: warm · Lift: needs-annotation · Priority: 2.80/5 Regime: safety-critical boundary Recommend: Phase 4 cap (ML_WITH_FALLBACK, never ML_PRIMARY) ... 5 more Report written to .postrule/analyze-2026-04-22.json
One product, priced on usage — not seats. Free until you’re at real volume.
Free covers both self-hosted and cloud up to 10K classifications/mo. Paid plans add team features and a longer audit trail. Most of what you pay, you’ve already saved on LLM calls. Every CTA goes to signup; Stripe Checkout is a post-signup flow.
Loading tier data…
Every paid tier has a published price. No "contact us" gating below Enterprise. Volume-priced so adding another classifier doesn't cost you another seat. Cancel anytime.
Design partner = first reference customer for an industry. We co-build the compliance artifacts (BAA, SOC 2 Type II, FedRAMP, FDA SaMD) with you; you become the named anchor; pricing reflects scope.
Find your tier · estimate your savings
Drag your monthly verdict volume; pick your current LLM. We
recommend the smallest tier that fits, and project your
post-graduation cost (assuming half your sites graduate to
in-process ML at ~$3 per million calls). Your
inputs only — nothing is sent.
Your platform fee never exceeds half of what Postrule saves you.
Assumes 70% of detected classification sites graduate to in-process ML within 90 days. Your actual share will vary with the report card.
Where Postrule has measurable impact today
- Customer-support triage
- Chatbot intent routing
- LLM output moderation / PII filtering
- Fraud and anomaly triage
- SOC alert classification
- Content moderation
- Clinical coding (ICD-10, CPT)
- RAG retrieval-strategy selection
- Agent tool routing
Four more categories in the full list.
What this replaces
| Approach | What it solves | Where Postrule differs |
|---|---|---|
| LLM-response caching (LiteLLM, langfuse cache) |
Cuts repeat-call cost on identical inputs. | Postrule retires the LLM tier permanently when an in-process ML head clears the gate — not only on cache hits. |
| Fine-tuning the LLM | Better LLM accuracy on your domain. | Postrule's terminal classifier is not a tuned LLM. It's a sklearn-class in-process head: sub-ms, ~$0/call, no GPU, no inference API. |
| Roll your own ML migration | Full control over outcome plumbing, training, shadow rollout. | Postrule is one decorator at every classification site, with a uniform statistical contract and audit chain — and the rule preserved as the safety floor. |
| Feature flags + manual ramp | Operator-driven rollout with an off switch. | Postrule's gate is statistical, not vibes-based: graduation only fires when accumulated outcomes prove the upgrade is real. |
When Postrule isn't the right fit. If your LLM bill is dominated by generation, summarization, or agent loops rather than classification, the savings story scales down — you'll get the safety-floor and audit-chain benefits but not the "$5,000/day retires itself" story. If you have abundant day-one labeled data, train a classifier directly. If your verdicts never arrive (no downstream signal, no human review, no inferred outcome), the gate has nothing to fire on.
Your AI coding assistant already knows how to install Postrule.
Postrule ships a SKILL.md that Claude Code, Cursor, and Copilot Workspaces can load as context. Just ask:
"Add Postrule to the triage function in
src/support/triage.py."
Your assistant will wrap the function, add the import, infer the labels, and leave a minimal, reviewable diff.
postrule init src/support/triage.py:triage --author "@you:team"
postrule init is the deterministic CLI path
that skips the LLM's risk of hallucinating decorator
syntax. Your assistant should reach for it by default.