Atomic, overdraft-proof, idempotent credit metering over Postgres. Extracted from PANTHEON.
Project description
credit-ledger
An atomic, overdraft-proof, idempotent credit ledger — for metering money on a pay-per-use or multi-tenant service. ~150 lines of Python over Postgres. Extracted from PANTHEON, a multi-tenant AI substrate, where it's the money path for autonomous AI agents that spend real credits on every turn.
The interesting part isn't the size — it's that three properties that are usually gotten subtly wrong are each guaranteed by a single, boring database mechanism instead of application-level hope:
| Property | How | Not by |
|---|---|---|
| No overdraft, ever — a balance can never go negative, even under a stampede of concurrent charges | one guarded UPDATE … WHERE credits >= n |
SELECT then UPDATE (a race), or a mutex (doesn't survive multiple processes) |
| Exactly-once under retries — a replayed charge (a Stripe webhook fired twice, a client retry) charges once | UNIQUE(tenant_id, idempotency_key) on an append-only ledger |
de-duping in app code (another race) |
| Tenant isolation — tenant A can't read or write tenant B's balance | Postgres row-level security, FORCEd |
a WHERE tenant_id = ? you have to remember to add every time |
The core: one statement does the hard part
A spend is a single guarded, atomic UPDATE:
UPDATE credit_balance
SET credits = credits - :n
WHERE tenant_id = :t AND credits >= :n
RETURNING credits;
- The row lock serialises concurrent charges against the same tenant — no lost updates.
- The
credits >= :nguard makes overdraft structurally impossible: the charge either fully succeeds (a row comes back) or changes nothing (zero rows). There is no window between the check and the debit, because they're the same statement.
That's the whole trick, and the test proves it: fire 100 concurrent spends of 1 credit against a balance of 50, and exactly 50 succeed — the balance lands at 0, never negative, with 50 ledger rows.
# tests/test_metering.py
def test_atomic_decrement_under_concurrency(engine, tenant):
metering.grant(tenant, 50, engine=engine)
with ThreadPoolExecutor(max_workers=20) as ex:
results = list(ex.map(lambda _: metering.decrement(tenant, 1, engine=pool).ok, range(100)))
assert sum(results) == 50 # exactly the balance, no more
assert metering.balance(tenant, engine=engine) == 0.0 # never negative
Idempotency: the ledger is the dedup
Every grant and spend appends a row to credit_ledger, which carries a UNIQUE(tenant_id, idempotency_key). Pass an idempotency_key (a Stripe event_id, a request id) and a replay can't insert a second row — the database rejects it, and the code returns the original result marked deduped=True. Money is never double-counted, and a duplicate never surfaces as a 500.
The subtle bit is the concurrent race: two identical charges arrive at once, both past the "already seen?" check, both try to insert. One wins; the other's INSERT violates the constraint, which rolls back its whole transaction — including the decrement. The loser then re-reads the ledger, finds the winner's row, and returns that. So a concurrent duplicate is a clean dedup, not a double-charge and not an error. There's a test for exactly this — 20 concurrent replays of one Stripe event → one credit applied, zero exceptions.
The correctness detail most people miss: fail loud, not phantom-success
An IntegrityError after retries is ambiguous — it could be a genuine concurrent dedup (the winner committed, there's a ledger row), or it could be a foreign-key violation because the tenant doesn't exist (nothing landed). Reporting success in the second case would silently lose money. So the code disambiguates by re-reading the ledger: a row means real dedup → report success; no row means nothing happened → fail loud (ok=False). This came out of an adversarial self-audit of the substrate; it's the difference between "looks fine" and "correct."
The RLS footgun this quietly avoids
Row-level security is only a guarantee if the writer is actually subject to it. Metering runs on a privileged app role (the money path is infrastructure), and on any managed Postgres that role is not BYPASSRLS. So before every transaction the code binds the tenant into a session variable the policy reads:
c.execute(text("SELECT set_config('app.current_tenant', :t, true)"), {"t": tenant_id})
Skip this and, the moment RLS is enforced in production, the guarded UPDATE matches zero rows and the INSERT's WITH CHECK fails — billing silently bricks, while every dev box (superuser, RLS bypassed) looks perfectly fine. Getting this right is the difference between a demo and something you'd run.
Why so little code
The database does the concurrency, the atomicity, and the isolation. The code's entire job is to use those primitives correctly — bind the tenant, guard the decrement, key the ledger, and disambiguate the one genuinely-ambiguous failure. Fewer moving parts is the point, not an accident.
Use it
pip install -e . # SQLAlchemy is the only runtime dep
createdb ledger && psql "$DATABASE_URL" -f schema.sql
from credit_ledger import metering
metering.grant(tenant_id, 100, idempotency_key="stripe:evt:abc") # top up (Stripe-retry-safe)
c = metering.decrement(tenant_id, 1, reason="chat-turn") # spend
if not c.ok:
... # out of credits — nothing was charged
metering.balance(tenant_id) # -> Decimal (exact, never float)
Every function also takes an explicit engine= (for your own pool/lifecycle). schema.sql is the full Postgres schema including the RLS policies.
Run the tests
DATABASE_URL=postgresql://localhost/ledger pytest -q # needs Postgres + schema.sql applied
The suite is the spec: overdraft impossibility under 100-way concurrency, idempotent spend and grant retries, concurrent Stripe-replay dedup, and rejection of non-positive grants (a negative "grant" would decrement a tenant — the primitive refuses it before touching the DB).
Benchmark: does it hold at load?
There's a reproducible load benchmark in benchmarks/ — it fires thousands of concurrent charges at a single balance row (the worst case: every charge serialises on the same lock) and re-checks the invariant afterwards. A real run on stock Postgres 16 in Docker (durable commit per charge, 32 workers), 10,000 charges against a balance of 5,000:
throughput ~1,100 charges/sec p50 ~22 ms · p95 ~77 ms · p99 ~138 ms
succeeded 5,000 (= the balance, exactly) rejected 5,000 (insufficient)
ledger rows 5,000 (one per real debit) final balance 0.0 (never negative)
INVARIANT HELD ✓
That's the pessimal number — one contended row, throughput bounded by a single durable round-trip. Charges spread across many tenants hit different rows and don't contend, so real throughput scales with Postgres, not this ceiling. The point isn't the speed; it's that under 10,000 racing charges colliding at the empty-balance boundary, not one overdrafts. See benchmarks/README.md for how to read it and run your own.
Design notes
A few deliberate choices, and the answers to the sharp questions they invite:
- A failed (insufficient) charge records nothing — so a retry with the same key re-attempts. That's intended. Idempotency dedups successful effects, not attempts. A charge rejected for insufficient balance didn't happen, so the same key should be free to succeed later — e.g. a metered webhook that hit a zero balance and is retried after a top-up. The dedup keys the committed ledger row, which only exists for charges that actually landed.
numeric(18,4)in the database,Decimalat the API boundary (since 0.2.0). The stored balance and every ledger delta are exact decimals — and amounts are coerced toDecimaland bound asDecimal(never handed to Postgres as a float to cast), and balances are returned asDecimal. So money is exact end to end: fractional per-call pricing, balances past 2⁵³, and arithmetic on the returned value all stay precise. Pass amounts asDecimal/int/str(afloatis accepted and coerced viaDecimal(str(x)), but preferstr/Decimalso you never introduce float error before it reaches us).- Why two attempts is enough (
for _attempt in range(2)). Under N concurrent charges sharing one key, exactly one wins theUNIQUE(tenant_id, idempotency_key)insert and commits; every loser'sINSERTraises, rolling back its whole transaction (decrement included). On the second pass the losers see the winner's now-committed ledger row in the "already seen?" read and return that — a clean dedup. A third attempt can't be needed: once the winner commits, the row is visible, so attempt two is always either the winner's own success or a loser's dedup. The re-read after the loop only exists to distinguish the genuinely-nothing-landed case (e.g. a foreign-key violation) — so a constraint error is never reported as phantom success.
Context
This is one self-contained piece of PANTHEON — a multi-tenant substrate for running AI agents safely on other people's money and data (tenancy + RLS isolation, a governed reasoning loop, an approval queue, this metering ledger, and a quality gate), designed and built solo. The live Studio at pantheonlabs.info runs on it; the flagship write-up is at pantheonlabs.co.uk. Built by Isaac Teague Frayling.
Changelog
- 0.2.0 —
Decimalend to end (breaking): balances returnDecimaland amounts are coerced to and bound asDecimal, instead of round-tripping money throughfloat. Thenumeric(18,4)column was always exact; now the API boundary is too, so fractional pricing / large balances / arithmetic on the result can't pick up IEEE-754 error.floatinputs are still accepted (coerced viaDecimal(str(x))). - 0.1.0 — initial release.
License
Apache-2.0. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file credit_ledger-0.2.0.tar.gz.
File metadata
- Download URL: credit_ledger-0.2.0.tar.gz
- Upload date:
- Size: 14.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
246a5c04d4b20349d6c5681dd3362c8ffe7f9c12fd76309d124fc57e6bf5a7d0
|
|
| MD5 |
e5c2f2dd5b6882540b787c35ee598d65
|
|
| BLAKE2b-256 |
6281a4fbb78a07ed49cf430fb3eee85cb81f44ac487c3e9da39e5236077025bf
|
Provenance
The following attestation bundles were made for credit_ledger-0.2.0.tar.gz:
Publisher:
publish.yml on Igfray/pantheon-credit-ledger
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
credit_ledger-0.2.0.tar.gz -
Subject digest:
246a5c04d4b20349d6c5681dd3362c8ffe7f9c12fd76309d124fc57e6bf5a7d0 - Sigstore transparency entry: 2163737568
- Sigstore integration time:
-
Permalink:
Igfray/pantheon-credit-ledger@8d8ee9704dfa5b8eba8475b0e60488fe24369e62 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Igfray
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d8ee9704dfa5b8eba8475b0e60488fe24369e62 -
Trigger Event:
push
-
Statement type:
File details
Details for the file credit_ledger-0.2.0-py3-none-any.whl.
File metadata
- Download URL: credit_ledger-0.2.0-py3-none-any.whl
- Upload date:
- Size: 13.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3af25fd9a0654e507afddb2168410e76f4e3843ac9960f008e131c646c21cda4
|
|
| MD5 |
72a505aabf7772958fe24dab230b676e
|
|
| BLAKE2b-256 |
3058bda6f61c1df493314f3a8a484e3ba76aa29f6c6971ab7b4e565f2bbc7f59
|
Provenance
The following attestation bundles were made for credit_ledger-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on Igfray/pantheon-credit-ledger
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
credit_ledger-0.2.0-py3-none-any.whl -
Subject digest:
3af25fd9a0654e507afddb2168410e76f4e3843ac9960f008e131c646c21cda4 - Sigstore transparency entry: 2163737575
- Sigstore integration time:
-
Permalink:
Igfray/pantheon-credit-ledger@8d8ee9704dfa5b8eba8475b0e60488fe24369e62 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Igfray
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d8ee9704dfa5b8eba8475b0e60488fe24369e62 -
Trigger Event:
push
-
Statement type: