Payment schemes
The three ways a solana_pay_kit gate charges — fixed charge, metered usage (upto), and session — and the decorator/dependency each uses.
A gate's scheme is decided by which gating primitive you mount. All three ship across the framework shims (the session gate is FastAPI-first).
| Scheme | Primitive | What it does |
|---|---|---|
| Fixed charge | require_payment / RequirePayment | Charge a fixed amount once, settled before the view runs. |
| Metered usage | require_usage / RequireUsage | Authorize a ceiling, meter actual usage, settle the real amount and refund the rest. |
| Session | RequireSession | Open a metered payment channel, bill per delivery, settle on idle-close. |
Usage gates settle via x402 upto, so they are x402-only and cannot carry fees. subscription and x402
batch-settlement are not implemented in Python.
Fixed charge
The default gate: a fixed price the client settles over MPP or x402 (its choice). Settlement runs before your view.
solana_pay_kit.configure(network="solana_localnet")gate = Gate.build( name="quote", amount=usd("0.01"), description="Stock quote", default_pay_to=solana_pay_kit.config().effective_recipient(), accept_default=solana_pay_kit.config().accept,)app = Flask(__name__)# @require_payment settles the 402 (MPP or x402, the client's choice) before# the view runs; the verified proof is readable via solana_pay_kit.flask.payment().@app.get("/quote")@require_payment(gate)def quote(): return jsonify(ok=True, protocol=payment().protocol.value)The client side pays in one call through the x402 transport — see Client:
# x402_async_client returns an httpx.AsyncClient that turns any 402 into a# signed PAYMENT-SIGNATURE payment and replays the request — transparently.async with x402_async_client(signer, rpc) as http: resp = await http.get("https://api.example.com/quote") print(resp.status_code, resp.headers.get("payment-response"))Metered usage (upto)
A usage gate advertises amount as the ceiling. The client opens a payment channel depositing that maximum; the handler meters real usage and reports it through the Charge dependency (charge.charge(base_units)); the gate settles the actual amount and refunds the remainder after the handler returns.
# A usage gate authorizes a ceiling (x402 `upto`); the handler meters actual# consumption via the Charge dependency and the gate settles that amount after# the request, refunding the remainder. Usage gates are x402-only.summarize_gate = Gate.build( name="summarize", amount=usd("0.10"), description="Summarize text, billed per token", default_pay_to=solana_pay_kit.config().effective_recipient(), accept=(Protocol.X402,),)app = FastAPI()install(app) # maps PayKitError -> 402 challenge@app.post("/summarize")async def summarize(request: Request, charge: Charge = Depends(RequireUsage(summarize_gate))) -> dict: body = await request.body() tokens = max(1, len(body) // 4) charge.charge(tokens * 100) # actual base units consumed return {"tokens": tokens}Session
A session gate opens a metered payment channel (MPP). RequireSession is the 402 gate — it verifies a credential/voucher or answers with a fresh challenge — and the reserve/commit metering side channel is mounted from session_routes. Settlement runs on idle-close when an operator signer and RPC are configured.
# One session method, built from the shared config. `cap` is the ceiling the# server offers; on-chain settle-at-close needs the operator signer + RPC.session = new_session( SessionOptions( operator=cfg.operator.signer.pubkey(), recipient=cfg.effective_recipient(), cap=1_000_000, # 1.00 USDC ceiling currency=resolve_mint("USDC", "mainnet"), decimals=stablecoin_decimals("USDC"), network=cfg.network.value, secret_key=cfg.mpp.challenge_binding_secret or "", signer=cfg.operator.signer, rpc=SolanaRpc(cfg.effective_rpc_url()), open_tx_submitter="server", close_delay=2.0, # idle-close watchdog ))challenge = SessionChallengeOptions(cap="1000000", description="Metered token stream")# RequireSession is the 402 gate: verify a credential/voucher or answer with a# fresh challenge — the session counterpart of RequirePayment.gate = Depends(RequireSession(session, challenge))@router.get("/stream")async def stream(_=gate) -> StreamingResponse: ... # stream metered deliveries, billed per chunk against the voucher# Mount the reserve/commit metering side channel built by session_routes.routes = session_routes(session.core())@router.post("/__402/session/deliveries")async def deliveries(request: Request) -> JSONResponse: r = await routes.deliveries(request.method, await request.body() or b"{}") return JSONResponse(r.body, status_code=r.status)