pay.sh docs
SDKPython

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

SchemePrimitiveWhat it does
Fixed chargerequire_payment / RequirePaymentCharge a fixed amount once, settled before the view runs.
Metered usagerequire_usage / RequireUsageAuthorize a ceiling, meter actual usage, settle the real amount and refund the rest.
SessionRequireSessionOpen 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)

On this page