Skip to main content

SESTER — Metering, Quota, Fail-Closed Policy & Verifiable Receipts for AI-Agent APIs

SESTER Chain-S mark

CI PyPI Python License Deps Discussions

SESTER — metering · policy · evidence

🪙 "If your agent is going to pay, you set the rules." Sester plugs x402-style metering, quotas, fail-closed spend policy and a tamper-evident hash-chain receipt ledger into your API — as one ASGI middleware, with zero required dependencies.

What it does, in one paragraph: every paid request passes through a 402 → payment → receipt handshake; spend is metered in integer minor units against per-agent daily quotas; a fail-closed policy engine (host allow-lists, time windows, escalation to human approval) decides before your handler runs; and every decision and charge lands in a hash-chained ledger that third parties can verify with nothing but sha256. Four agent-commerce protocols — x402, AP2, ACP, UCP — compile onto one wire-format core (ChargeIntent/ChargeReceipt), so adding a fifth protocol is one adapter, not a rewrite.

30 seconds: what is this?

Sester is a payment-metering + receipt layer for APIs that AI agents call. It does not move funds (non-custodial). It does three concrete things:

  1. 402 gate — every paid request must present a valid payment envelope or the request is rejected (fail-closed, never "free on error").
  2. Spend rules — per-agent daily quotas, per-request price caps, host allow-lists, burst limits, human-escalation for large spends.
  3. Verifiable receipts — every charge lands in a hash-chained ledger and yields a receipt that anyone can verify with sha256 alone, no Sester installed, no secrets. This is the piece the x402 ecosystem is missing: a facilitator can say "paid", but without a receipt the receiver cannot prove it (USENIX Security 2026 found rule violations in all 15 major x402 facilitators — see docs/arastirma/).

Zero required dependencies (pure stdlib ASGI middleware). Try it for $0 — local embedded ledger, no keys, no testnet:

pip install "sester[demo]"
uvicorn sester.demo_api:app --port 8402          # paid API up
python -m sester.demo_api --mac agent-1:n1:0.05 /weather
curl -H "X-Sester-Agent: agent-1" \
     -H "X-Payment: <envelope-from-above>" \
     "http://127.0.0.1:8402/weather?city=istanbul"   # 200 + X-Sester-Receipt

And the proof layer (no server needed at all):

export SESTER_LEDGER_DB=/tmp/l.sqlite3 SESTER_LEDGER_SECRET=demo
# (in your app, a charge already happened — produce its receipt:)
sester receipt 1 --out receipt.json
# ...hand receipt.json to a third party. They verify WITHOUT Sester:
sester verify receipt.json                          # KABUL (rc=0) / RED (rc=1)

AI agents can do both through an MCP server (Model Context Protocol 2025-06-18) — mcp/: pay, verify_receipt, balance, history.

Sester is built on one doctrine: every failure path denies spend. A corrupt policy file → DENY_ALL. An unknown payment scheme → 402. A tampered evidence bundle → loud RED, never a receipt. A facilitator outage → 402, not a free call. Silent fallbacks (except: pass) are treated as security bugs — the test suite pins them. If you are metering money, "fail open" is not a mode, it is a leak.

3-minute demo

# 0) Install (demo extras pull in FastAPI + uvicorn)
pip install "sester[demo]"

# 1) Start the demo API (port 8402)
uvicorn sester.demo_api:app --port 8402

# — or, one command with Docker —
# docker run --rm -p 8402:8402 ghcr.io/goun7/sester-demo:latest

2) Prepare a payment envelope (HMAC issuer)

python -m sester.demo_api --mac f1-telemetry:n1:0.05 /weather

3) Send the paid request (paste the envelope from step 2)

curl -H "X-Sester-Agent: f1-telemetry"
-H "X-Payment: pugio0 f1-telemetry:n1:0.05:"
"http://127.0.0.1:8402/weather?city=istanbul"

4) Open the panel: http://127.0.0.1:8402/panel


The flow: `curl` without payment → **402 + `X-Payment-Required` challenge**;
with payment → **200 + `X-Sester-Receipt`** evidence header; the 5th call →
**402 quota exceeded**; `/panel` shows who called, how much, and a live
chain-integrity badge.

Since v0.7.4 the paid 200 also carries `X-Sester-Receipt-Bundle` — a base64
**node-cosigned receipt** the caller can hand to a third party who verifies it
without Sester:

```bash
# 5) (optional) cosign the demo receipts; the default is cosign-free but
#    the receipt's `proof` is still independently verifiable with sha256 alone
export SESTER_DEMO_NODE_SECRET=demo-node-key
# restart uvicorn, redo the paid call, then:
curl -s -D - -o /dev/null -H "X-Sester-Agent: agent-1" \
     -H "X-Payment: $PAY" "http://127.0.0.1:8402/weather" \
  | grep -i 'receipt-bundle:' | cut -d' ' -f2 | base64 -d > receipt.json
sester verify receipt.json          # KABUL (rc=0) — receiver-side, zero deps

Use it in your own app (one middleware)

from sester import SesterMeter, Policy, Ledger

policy = Policy.from_dict({
    "wallet_policy": {
        "id": "my-api",
        "defaults": {
            "per_request_max": 0.10,       # one call cannot cost more than 10¢
            "daily_max": 5.00,             # ...and one agent cannot pass $5/day
        },
        "rules": [
            {"id": "allow-telemetry", "when": {"host_in": ["/telemetry"]}, "then": "allow"},
            {"id": "approve-large",   "when": {"amount_gt": 0.05}, "then": "escalate"},
            {"id": "deny-rest",       "when": {"host_in": []},     "then": "deny"},
        ],
    }
})

meter = SesterMeter(
    app=your_asgi_app,
    ledger=Ledger("usage.sqlite3", secret=SESTER_SECRET),
    policy=policy,
    price=0.01,                        # per-request price in major units
    burst_capacity=20,                 # optional: token-bucket second gate
    burst_refill_per_sec=10.0,
)
# any ASGI server: uvicorn your_module:meter

That one wrapper gives you: the 402 handshake, per-request pricing, the integer-minor-unit quota, first-match-wins policy evaluation with a fail-closed default, burst limiting independent of the daily quota, and the hash-chained receipt ledger that any third party can verify with sha256 alone (see docs/K0_SHARED_ENVELOPE_SPEC.md). Escalation rules open a human-approval ticket instead of denying outright (then: "escalate").

Feature map (by versions)

Area What you get Since
Metering middleware x402-style 402 handshake, per-request pricing, quota in integer minor units v0.1
Policy engine First-match DSL, host allow-lists, time windows, fail-closed defaults v0.1
Evidence ledger Hash-chained SQLite (Postgres parity) receipts, HMAC-sealed, verify_chain() v0.1
Settlement x402 v2 verify/settle facilitator with injectable transport, fail-closed on outage v0.2
Protocols AP2 mandates (AP2-Mandate), ACP checkout sessions (ACP-Session), UCP web-monetization (UCP-Checkout) → one core v0.2–0.4
Human approval then: escalate → 402 escalation ticket, one-time consumption, TTL expiry, /approvals panel v0.2
Signed mandates RFC 7515 JWS: HS256 (stdlib) + ES256 ([jws]), body-binding, alg-allowlist v0.3
Signed policy Owner-signed policy envelopes; tightening is instant, loosening is delayed 24 h v0.3
Postgres Identical hash-chains across SQLite/PG (same secret + events → same head) v0.3
Migration SQLite → PG hash-preserving replay, nonces preserved, --plan/--dry-run/--verify v0.3.1
On-chain batches Pure-stdlib keccak-256, Merkle root recomputable in EVM, ABI settle(...) calldata, non-custodial v0.4
Minor-unit column amount_minor with hash-preserving migration — quota decisions end-to-end integer v0.4
Hosted facilitator FastAPI service: verify/settle/refund + seller metering (free band + 1% + $0.005) v0.5
Observability GET /metrics — Prometheus-text counters (requests, charges, replay/quota/rate 402s, chain-valid gauge) v0.6
Burst limiting Per-agent token-bucket (burst_capacity, burst_refill_per_sec) — independent of the daily quota v0.6
Evidence webhooks HMAC-signed delivery of charge/settlement events with receiver-side verification, retry+backoff, ledger failure-log v0.6
Meter package Single public import — from sester import SesterMeter, Policy, Ledger, ...; __all__ is the locked surface v0.7.2
Evidence export External-verifier bundles — anyone can audit with sha256 alone, no Sester installed v0.3+
Payment receipts Node-cosigned receipt per charge: proof recomputable with sha256 only; sester verify is the standalone receiver-side CLI v0.7.4
CLI sester verify | receipt | history | bundle — receiver-side proof verification with zero deps installed v0.7.4
MCP server AI agents pay and verify receipts: pay, verify_receipt, balance, history (MCP 2025-06-18, stdio, stdlib) v0.7.4

Policy template bank

Copy a template, edit the limits, run. Every template is machine-locked by tests/test_policy_bank.py: it must pass the DSL validator and carry at least one allow rule plus one guardrail (deny/escalate).

Template Pattern Key rules
examples/f1_policy.json human-escalation for large spends require-human-for-large (escalate), allow-demo-endpoints, deny-unknown-hosts
examples/fleet_lane/policy.json business-hours fleet lane allow-telemetry (host_in + hour_between 07:00–23:00), deny-unknown-hosts
examples/budget_guard_policy.json tiered budget guard two amount_gt thresholds — deny >10, escalate >1 — then host allowlist, then deny

DSL conditions (closed set — unknown keys are rejected at load time, so a typo like hostt_in fails loudly instead of silently never matching):

Condition Matches when Since
host_in request path is in the list; empty list = catch-all v0.1
hour_between wall-clock is inside ["HH:MM","HH:MM"] (wraps midnight) v0.1
amount_gt price is strictly above the threshold v0.1
hour_in wall-clock equals one of ["HH:MM", ...] exactly — minute-sharp windows v0.7.3
agent_in caller's agent id is in the list (case-insensitive); skipped when no agent is declared v0.7.3

First match wins; no match → deny (fail-closed, test_bank_template_validates).

Payment receipts — proving "paid" without trusting the payee

The gap: x402 facilitators verify a payment and return a settlement response, but a receiver (an auditor, a buyer's employer, a marketplace) gets nothing it can independently re-check later. USENIX Security 2026 (arXiv:2607.19545) found rule violations in all 15 major facilitators it tested. Sester's answer is a single, boring, verifiable object:

{
  "receipt_version": 1,        "iss": "sester:node",
  "sub": "agent-1",            "resource": "/weather",
  "amount": "0.050000",        "amount_minor": 50000,
  "ts": 1789456789.123456,     "seq": 7,
  "proof": "<sha256 hex>",     "prev_proof": "<prev sha256 hex>",
  "chain_head": "<sha256 hex>","node_cosign": "<HMAC hex>"
}

The proof is sha256(ts | event_type | agent | resource | amount | payload | prev_proof) — recomputable with pencil and a hash function. Tampering with any of those fields breaks it; tampering with the metadata breaks the optional node co-signature. The receiver needs nothing but sha256:

# receiver side — Sester is NOT installed
import hashlib, json
r = json.loads(receipt_text)
assert r["receipt_version"] == 1
manual = "|".join([f"{float(r['ts']):.6f}", r["event_type"], r["sub"],
                   r["resource"], f"{float(r['amount']):.6f}",
                   r["payload"], r["prev_proof"]])
assert hashlib.sha256(manual.encode()).hexdigest() == r["proof"]   # proved
# or, one command — rc=0 KABUL, rc=1 RED
sester verify receipt.json

Schema rationale, field-by-field security analysis and comparison against EIP-3009 / JWS / Merkle bundles: docs/arastirma/02_receipt_schema.md.

MCP server — agents pay, agents verify

mcp/server.py speaks Model Context Protocol 2025-06-18 over stdio (JSON-RPC 2.0, pure stdlib) and exposes four tools:

Tool Purpose
pay charge an agent for a resource → returns the node-cosigned receipt
verify_receipt verify any receipt independently (receiver-side)
balance daily spend / quota / remaining per agent
history hash-chain events

Embedded mode runs a local ledger — $0, no keys, no testnet. Remote mode (SESTER_MCP_ENDPOINT) pays a real Sester-protected API. Publishable to Smithery/Arcade (mcp/smithery.yaml): full setup in mcp/README.md.

Sester does not merge with its sisters — it bridges them. sester.bridges emits receiver-independent evidence envelopes; the counterpart verifies without importing Sester (stdlib-only, the dogrula.py discipline):

Bridge Direction Function Receiver needs
K1 · Tamga Sester → Tamga ledger tamga_anchor() / tamga_anchor_json() nothing but the anchor JSON
K2 · Veridict Sester → Veridict jury veridict_claims() / veridict_claims_json() nothing but the claims JSON
from sester.bridges import tamga_anchor_json, veridict_claims_json, BRIDGE_VERSION

anchor = tamga_anchor_json(bundle)        # deterministic JSONL envelope
claims = veridict_claims_json(bundle)     # claim_id = sha256(task|summary|v)[:16]

Both bridges work on public fields only — non-custodial and secretless-verifiability are preserved. BRIDGE_VERSION pins the wire contract; receivers reject unknown versions (K0 §7 rule 2). Cross-repo CI (Cross-repo bridges job) verifies both ends on every push.

Protocol adapters

Protocol Header Envelope Signature
x402 (HMAC compat) X-Payment pugio0 agent:nonce:amount:mac HMAC-SHA256
x402 v2 (EVM) X-Payment EIP-3009/EIP-712 exact scheme wallet address = agent identity
AP2 AP2-Mandate base64 JWS mandate HS256/ES256, body-binding
ACP ACP-Session base64 JWS checkout session JWS + vendor-side issuing
UCP UCP-Checkout base64 JWS web-monetization vendor-sealed, merchant-bound

All four compile to the same ChargeIntent/ChargeReceipt core — one shared wallet means one shared quota. Register your own via register_scheme / ProtocolAdapter.

Test suites & acceptance runs

.venv/bin/python -m pytest tests/ -q    # full suite: policy, ledger, middleware, EVM schemes,
                                        # evidence, facilitator, protocol adapters, escalation,
                                        # JWS, signed policy, UCP, settlement, minor units,
                                        # payee registry, S6 joint acceptance, receipts, CLI,
                                        # MCP server — 380+ passing tests
                                        # (9 env-gated skips: PG/sibling-repos/external tools)
python scripts/s1_dogfood.py            # S1 acceptance scenario → KABUL (accepted)
python scripts/dogrula.py adoption/s1-kanit-bundle.json   # receiver side — no Sester needed
sester verify receipt.json                                # standalone proof CLI (rc=0 KABUL / rc=1 RED)

Cross-repo bridge tests (tests/test_bridges_crossrepo.py) run external evidence receivers end-to-end via subprocess (clean → ACCEPT, tampered → RED). They skip automatically when the counterpart repos are absent — the embedded pure-stdlib mirrors in bridge_receivers/ pin the same wire contract in every run.

Documentation

Document Contents
docs/K0_SHARED_ENVELOPE_SPEC.md Shared evidence-envelope wire contract (external anchors, audit feeds)
docs/arastirma/ Academic security research (USENIX'26 x402 facilitator study, Tamarin analysis, AP2 whisper attacks) + receipt-schema rationale + MCP market gap
docs/ROADMAP.md Shipped by version, what's next, deliberate non-goals
Architecture decision records Scope, doctrine, deliberate limits — see the repository's decision documents
Policy DSL specification Semantics + test-vector discipline — see the repository's spec documents
Execution plan Acceptance milestones (S1–S6) — see the repository's plan documents
CONTRIBUTING.md Hard rules for PRs (fail-closed, K0-frozen, test-first)
SECURITY.md Reporting policy — replay/quota/signature-bypass bugs

Deliberate v0 limits

Non-goals by decision, not by omission (see the repository's decision records): transaction signing stays out of the library (non-custodial — the signing party is yours), live PSP certification is pending real-world traffic, and the receipt/CLI/MCP surface covers proof-of-payment, not LLM intent binding (the third AP2 "whisper" attack is a decision-layer problem — see docs/arastirma/01_x402_guvenlik_aciklari.md §3). Everything else on the roadmap through v0.5.0 is implemented and gated by the test suite above.

Environment variables

All optional; every one defaults to a demo/development value and warns when in effect — the production secrets never live in the repository.

Variable Default Meaning
SESTER_FACILITATOR_SECRET (none) Fail-closed, no default — the x402 facilitator service refuses to start without it (it derives the auth key). This is the one variable with no demo fallback.
SESTER_DEMO_SECRET sester-demo-secret-v0 Ledger seal for the demo; a startup warning is printed when the default is in effect.
SESTER_UCP_SECRET / SESTER_AP2_SECRET sester-demo-ucp / sester-demo-ap2 Per-protocol adapter secrets for the demo's four-protocol flow.
SESTER_DEMO_STRICT 0 Set 1 to make the demo fail-closed on unsigned ACP/UCP envelopes (default tolerates them for the demo light-path).
SESTER_DEMO_NODE_SECRET / SESTER_DEMO_NODE_ID (none) / sester:demo-node Cosign-key for demo receipts. Without it the receipt's proof is still sha256-verifiable; with it the response also carries a node co-signature (v0.7.4).
SESTER_ESCALATION_DB / SESTER_DEMO_LEDGER_DB sester-escalation.sqlite3 / sester-demo.sqlite3 DB paths — also used for test isolation (conftest moves these into tmp dirs).
SESTER_PG_DSN (none) Postgres parity: when set, PgLedger is exercised (the PG test-legs are skipped otherwise).
SESTER_TAMGA_REPO / SESTER_VERIDICT_REPO (none) Enable the cross-repo CI job that pins K1/K2 bridge compatibility against the sibling products' current main.

Docker (zero-setup demo)

The demo API — 402 challenge, HMAC/EVM/UCP payment flows, live panel — ships as a single image. All state lives in an isolated SQLite ledger inside the container; mount a volume to keep receipts across restarts.

docker run --rm -p 8402:8402 ghcr.io/goun7/sester-demo:latest

# persistent ledger:
docker run --rm -p 8402:8402 -v "$PWD/ledger:/data" \
  -e SESTER_DEMO_LEDGER_DB=/data/sester-demo.sqlite3 \
  ghcr.io/goun7/sester-demo:latest

Then run the curl flow above against http://127.0.0.1:8402.

License

Dual-licensed Apache-2.0 OR AGPL-3.0 — pick either per integration (see LICENSE). Apache for commercial rails that cannot touch copyleft; AGPL keeps network-service derivatives open. The frozen wire fields (pugio0, pugio_bundle_version, source: "sikke") are kept for receiver compatibility and are documented in the repository's identity-migration record; visual and wire identity are separate layers.

Metadata

Release files for sester 0.7.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sester 0.7.4
File Size Uploaded
sester-0.7.4.tar.gz 89.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sester 0.7.4
File Interpreter ABI Platform
sester-0.7.4-py3-none-any.whl Python 3 none any Details

Total release size: 194.0 kB

Release files / sester-0.7.4.tar.gz

Download URL sester-0.7.4.tar.gz
Size 89.2 kB
Tags Source
SHA-256 checksum
How to use checksums
60f2c738822ce2a82ce9fee6a8d9c599dcedbde3497ba9cc40f7ae84b2e86941
BLAKE2b-256 checksum
How to use checksums
f043fdee207afb828ef14619825f163708ebdcce654671332ec1f4c078698dc5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / sester-0.7.4-py3-none-any.whl

Download URL sester-0.7.4-py3-none-any.whl
Size 104.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fc1c9f8bd85d69286b432c8cf46827170b9c6855ffcbbe9c812bf2cdd8b760a0
BLAKE2b-256 checksum
How to use checksums
3c546ff03c2ce113b4acfd6decb78b51033177c2648e090dcd1107287ad0671a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

This release

0.7.4 This release

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page