Examples

Per-unit (metered) pricing

Compute the cost from the request instead of a fixed number:

from fastapi import Depends, Request
from gringotts import CreditedUser, charge


def cost_from_rows(request: Request) -> int:
    return int(request.headers.get("X-Rows", "1"))


@app.post("/bulk")
def bulk(user: CreditedUser = Depends(charge(cost_from_rows))):
    return {"credits_left": user.credits}

A negative or non-integer cost is rejected with 400 — a request can never mint credits.

Granting credits in code

grant (an alias for the ledger-safe credit function) adds credits with a ledger row. Pass a unique external_id to make event-driven crediting idempotent:

from gringotts import grant
from gringotts.db import SessionLocal

with SessionLocal() as db:
    user = ...  # look up your user
    grant(db, user, 100, kind="promo", external_id="signup-bonus-42")

Idempotent requests

Pass an Idempotency-Key header and a retried request applies exactly once: the first request runs and its response is stored; the retry returns that stored response without re-running the handler — so the charge happens once and the retry gets the original result back (with an Idempotent-Replayed: true header):

For host routes, configure a synchronous validator that rechecks mutable authorization before cached data is returned. If X-API-Key is the only such state, Gringotts already validates it and the callback can allow replay:

gringotts.init_app(
    app,
    GringottsConfig(idempotency_replay_validator=lambda _scope: True),
)

Without a validator, the first operation still runs once and remains locked, but a retry returns 409 instead of bypassing host authorization to expose cached data. Built-in Gringotts routes revalidate their own authorization and need no callback.

# both calls together charge once; the second returns the first's response
curl -X POST localhost:8000/predict \
  -H "X-API-Key: gk_..." -H "Idempotency-Key: order-42"
curl -X POST localhost:8000/predict \
  -H "X-API-Key: gk_..." -H "Idempotency-Key: order-42"

Keys are scoped to the caller, so one caller can’t replay another’s key. Reusing a key for a materially different request (method, path, any header, or body) returns 409. This includes authorization and dynamic-pricing headers, preventing a replay from crossing application principals or operation inputs. A raised error whose debit was refunded releases the key, so a genuine retry can re-attempt. A handler that returns a 5xx leaves its debit committed, so that response is cached. Responses marked no-store or too large to retain replay a marker instead of their original body. The same protection covers the admin grant route and every other mutating endpoint your app serves.

Admin API

Any user with the admin flag can manage users and credits over HTTP (JSON for scripts, an HTML dashboard for browsers):

gringotts create-user ops --admin

curl localhost:8000/gringotts/admin/stats -H "X-API-Key: gk_<admin>"
curl -X POST localhost:8000/gringotts/admin/users/3/grant \
  -H "X-API-Key: gk_<admin>" -d "amount=50"

Operations from the CLI

gringotts add-credits alice 100
gringotts balance alice
gringotts reconcile   # flag any balance that disagrees with the ledger
gringotts migrate     # apply pending schema changes to an existing database

Seeded demo

python examples/seed_demo.py    # 5 users + two weeks of simulated traffic
uvicorn examples.demo_app:app

The script prints each API key once; open /gringotts/admin with the admin key.