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.

Lower bills, not lower accuracy Moving a task off the model only when it’s safe beats always using one: higher accuracy with a third less labeled data, across 19 datasets.
Pennies per call Once a classification runs in-process, every call is under 2 ms at near-zero cost.
Adopt in an afternoon Add a decorator; your code stays put and your original logic remains the safety net.
No lock-in Apache-licensed SDK; the rest converts to Apache in 2030. Patent-pending.

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.

Pick a repo:
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).

Per-call cost from your LLM provider

Click a provider to set the slider to a typical classifier-call cost at that rate.

Estimated annualized savings
Per day, post-graduation
Sites high-priority (score ≥ 2.0)

Want to run this on your private repo? Same scan, same JSON, on your own machine in under a minute.

Install in 5 minutes → runs locally · your code stays on your machine · no signup

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.

⌘/Ctrl + Enter to run · first run loads Pyodide (~5 s)

Results

Results render here — function name, pattern, labels, regime, volume, priority, lift status, and the matched source range. Same shape as the local CLI output.

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. 1

    Install

    No GPU, no torch, no service deps. Sklearn-class only.

    pip install postrule
    Successfully installed postrule-1.3.0

    ~30s on a warm pip cache, up to a minute cold. uv pip install postrule works too if you've moved on.

  2. 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 --json for the machine-readable shape the website demo uses, or --project-savings --format markdown for the full savings breakdown.

  3. 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) == expected

    Two 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-run to write them in place, or add --no-test to skip the test.

  4. 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-lift adds 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. 5

    Run your code as usual

    No command. The wrapper does the work.

    Outcomes log silently to runtime/postrule/<switch>/ every time your app calls triage_ticket(...). Phase 0: the rule still decides; the switch just records. Hand the same record IDs to switch.record_verdict(record_id, Verdict.CORRECT) when downstream signals (CSAT, resolution code, human review) tell you whether the classification was right.

  6. 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.5

    Self-measured from your own outcome logs. Override the assumption bands with --engineer-cost-per-week / --monthly-value-low / --monthly-value-high.

  7. 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 calls postrule init and postrule benchmark for 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.

See the full sample report cards →

$ 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.

  1. RULE
    Your function decides.
  2. MODEL_SHADOW
    Rule decides; LLM watches.
  3. MODEL_PRIMARY
    LLM decides; rule is fallback.
  4. ML_SHADOW
    ML trains from outcomes.
  5. ML_WITH_FALLBACK
    ML decides; on uncertainty falls through LLM, then rule.
  6. ML_PRIMARY
    ML 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.

Eight representative panels: codelangs, ATIS, TREC-6, AG News, Snips, HWU64, Banking77, CLINC150. Each panel plots the hand-written rule's flat accuracy against the ML head's rising accuracy as outcomes accumulate. A dashed vertical line marks the first checkpoint where the evidence gate cleared.

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).

+7.5 pp Accuracy vs. always-graduate-to-ML (95% CI 2.4–12.7).
34% fewer Labeled examples to get there.
+86 pp Largest rule → ML accuracy gap (Banking77).

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.

Bar chart: rule baseline (grey) vs final ML-head accuracy (blue), eight representative benchmarks, sorted by rule baseline. codelangs at 87.8% rule, ATIS at 70.0%, TREC-6 at 43.0%, AG News at 25.9%, Snips at 14.3%, HWU64 at 1.8%, Banking77 at 1.3%, CLINC150 at 0.5%. Background tinting hints at the three-regime taxonomy.

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.

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.

See it on your codebase → 5 minutes, locally — your code stays on your machine.

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

Get the analyzer →

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.

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.