x402 payment-gated routes for Sanic: ask for payment with a single decorator
Project description
sanic-x402
Payment-gated routes for Sanic. Charge stablecoin micropayments for any endpoint with a single decorator, powered by the x402 protocol.
@app.get("/premium")
@paid("$0.01")
async def premium(request):
return json({"data": "..."})
Why sanic-x402?
- One decorator to monetize an endpoint: no accounts, no API keys, no payment processor. Clients pay per request over HTTP 402.
- Built on the official x402 SDK: speaks both x402 v1 (
X-PAYMENT) and v2 (PAYMENT-SIGNATURE) wire formats, works with any facilitator, and serves the built-in HTML paywall to browsers. - Safe by default: payments settle only after your handler succeeds. Failed responses are never charged.
- AI-agent ready: agents holding a funded wallet can discover the price from the 402 challenge and pay autonomously.
- Stays out of your way: plain Sanic middleware and decorators, no base classes, no app rewrites.
Feature Overview
| Feature | Description |
|---|---|
@paid decorator |
Gate a route with a price string like "$0.01" |
| Route kwarg | Gate via ctx_x402="$0.05" without importing anything |
| Path patterns | Gate whole subtrees, like the official Express/FastAPI middlewares |
| Dynamic pricing | Compute price or receiver per request with a callable |
| Multi-network | Offer several payment options (EVM, Solana, and more) per route |
| Browser paywall | Human visitors get a hosted payment page, agents get JSON |
| Settlement receipts | Successful responses carry a PAYMENT-RESPONSE header |
| sanic-ext support | Optional config-driven setup for sanic-ext users |
Quick Start
Install the package (the EVM extra covers the default Base/USDC setup):
pip install sanic-x402[evm]
Attach it to your app and mark a route as paid:
from sanic import Sanic
from sanic.response import json
from sanic_x402 import X402, paid
app = Sanic("Api")
X402(app, pay_to="0xYourReceivingAddress") # Base Sepolia testnet by default
@app.get("/free")
async def free(request):
return json({"hello": "world"})
@app.get("/premium")
@paid("$0.01")
async def premium(request):
"""Premium market data."""
return json({"data": "..."})
Unpaid requests to /premium receive 402 Payment Required with a signed challenge. Paid requests are verified with the facilitator, your handler runs, and the payment settles on-chain. The settlement receipt is returned in the PAYMENT-RESPONSE header.
Going to production on Base mainnet:
X402(
app,
pay_to="0xYourReceivingAddress",
network="eip155:8453",
facilitator="https://your-facilitator.example", # e.g. Coinbase CDP
)
The default facilitator is the testnet facilitator at https://x402.org/facilitator. It is an API base URL, not a web page: the bare path returns 404, while GET /facilitator/supported lists its capabilities.
Three Ways to Gate a Route
The @paid decorator (recommended). Apply it under the route decorator:
@app.get("/reports/<report_id>")
@paid("$0.25", description="Full analyst report", mime_type="application/json")
async def report(request, report_id):
...
A route kwarg. No extra import needed:
@app.get("/premium", ctx_x402="$0.05")
async def premium(request):
...
ctx_x402 accepts a bare price, a dict of @paid kwargs, an x402 PaymentOption, or a full RouteConfig.
Path patterns. Gate whole subtrees, like the official Express and FastAPI middlewares:
from sanic_x402 import X402, RouteConfig, PaymentOption
X402(app, routes={
"GET /api/premium/*": RouteConfig(accepts=[
PaymentOption(scheme="exact", pay_to="0x...", price="$0.10",
network="eip155:8453"),
]),
})
Supported Currencies
x402 is asset-agnostic: a payment option names any token on any supported network, and sanic-x402 exposes all of it.
USD money strings like "$0.01" are the simple path. They resolve to the network's default USD stablecoin from the SDK registry, which covers USDC on Base, Base Sepolia, Polygon, Arbitrum One, Arbitrum Sepolia, Monad, XDC and others, plus USDT0 (Stable), MegaUSD (MegaETH), Mezo USD, and more.
Any other token (EURC, DAI, or your own ERC-20/SPL asset) works via asset=:
@paid(
"€0.50",
asset="0x808456652fdb597867f38412077A9182bf77359F", # EURC on Base
asset_decimals=6,
asset_extra={"name": "EURC", "version": "2"}, # token's EIP-712 domain
)
async def handler(request): ...
The price is converted to atomic units with asset_decimals, and currency symbols or codes in the string are stripped. For full control, pass an AssetAmount directly as the price:
from sanic_x402 import AssetAmount
@paid(AssetAmount(amount="500000", asset="0x8084...", extra={"name": "EURC", "version": "2"}))
Two practical constraints apply. On EVM networks with the exact scheme, the token must support EIP-3009 transferWithAuthorization (USDC and EURC do) or the Permit2 flow. And your facilitator must support the scheme and network pair; check GET <facilitator>/supported. The v2 spec also allows ISO 4217 codes (like "USD") as the asset for fiat facilitators.
Advanced Usage
Dynamic pricing and receivers
price and pay_to accept callables that receive the x402 HTTPRequestContext:
@paid(lambda ctx: "$1.00" if ctx.adapter.get_query_param("hd") else "$0.10")
Multiple payment options per route
from sanic_x402 import PaymentOption, paid
@paid(accepts=[
PaymentOption(scheme="exact", pay_to="0xEvm...", price="$0.01",
network="eip155:8453"),
PaymentOption(scheme="exact", pay_to="Sol...", price="$0.01",
network="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"),
])
async def handler(request): ...
Non-EVM schemes must be registered explicitly:
from x402.mechanisms.svm.exact import ExactSvmServerScheme
X402(app, pay_to="...", schemes=[
("eip155:*", ExactEvmServerScheme()),
("solana:*", ExactSvmServerScheme()),
])
Payment info in handlers
After verification, request.ctx.x402 holds a PaymentInfo with the decoded payload and matched requirements:
@app.get("/premium")
@paid("$0.01")
async def premium(request):
payment = request.ctx.x402
return json({"paid_on": payment.requirements.network})
Paywall customization
from sanic_x402 import PaywallConfig
X402(app, pay_to="...", paywall=PaywallConfig(app_name="My API", testnet=True))
sanic-ext integration
Install with pip install sanic-x402[ext], then configure everything through app config:
from sanic_ext import Extend
from sanic_x402.ext import X402Extension
app.config.X402_PAY_TO = "0xYourAddress"
app.config.X402_NETWORK = "eip155:8453"
app.extend(extensions=[X402Extension])
How It Works
- A client requests a gated route without payment.
- sanic-x402 responds
402with a challenge (JSON for agents, an HTML paywall for browsers) describing price, asset, network, and receiver. - The client signs a payment authorization and retries with a
PAYMENT-SIGNATUREheader. - The payment is verified with the facilitator, then your handler runs.
- On a successful response the payment settles on-chain and the receipt is attached as a
PAYMENT-RESPONSEheader.
Semantics and Caveats
- No charge on failure. If the handler raises or returns a 4xx/5xx, settlement is skipped and the payment authorization expires unspent.
- Settlement runs after the handler, in response middleware. If settlement fails, the client receives
402with the failure receipt instead of being charged for the body. - Streaming. Responses returned from the handler (including
ResponseStream) settle before the body is sent. Mid-handlerawait request.respond()triggers settlement atrespond()time, so prefer returned responses on paid routes. - Websockets are not gated. A warning is logged if
@paidis applied to one. - Facilitator sync is lazy. Capabilities are fetched on the first protected request; pass
sync_facilitator_on_start=Falseto disable.
Development
uv sync
uv run pytest
Tests run fully offline against a fake facilitator.
Resources
License
MIT. 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 sanic_x402-0.1.0.tar.gz.
File metadata
- Download URL: sanic_x402-0.1.0.tar.gz
- Upload date:
- Size: 184.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
663b942ae922757065162c2255dd44d2bd8e0ac8ad398acd18eae4e0996d9224
|
|
| MD5 |
8dbfb1ddc2b08b54a5e7edcc6d3746e9
|
|
| BLAKE2b-256 |
20559336cd3c210030d3385c9d6dba98d78bc0ab8d38db0be4bc6eaf1d1a67d8
|
Provenance
The following attestation bundles were made for sanic_x402-0.1.0.tar.gz:
Publisher:
release.yml on victorlane/sanic-x402
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sanic_x402-0.1.0.tar.gz -
Subject digest:
663b942ae922757065162c2255dd44d2bd8e0ac8ad398acd18eae4e0996d9224 - Sigstore transparency entry: 2064451214
- Sigstore integration time:
-
Permalink:
victorlane/sanic-x402@263e39c33060e8103a0a0c4b065112e453e0618c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/victorlane
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@263e39c33060e8103a0a0c4b065112e453e0618c -
Trigger Event:
push
-
Statement type:
File details
Details for the file sanic_x402-0.1.0-py3-none-any.whl.
File metadata
- Download URL: sanic_x402-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.2 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 |
96e6dd80b514d54ccd7e50afdf294dff659da913b779e4ecaaa64917b4f561bb
|
|
| MD5 |
34e53cb4408b71be53e2364b9cf058cf
|
|
| BLAKE2b-256 |
9663a967e85946f4a9e7b0f847bdd73d7f92eaa66d4c7fddd1ea6a161fd76474
|
Provenance
The following attestation bundles were made for sanic_x402-0.1.0-py3-none-any.whl:
Publisher:
release.yml on victorlane/sanic-x402
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sanic_x402-0.1.0-py3-none-any.whl -
Subject digest:
96e6dd80b514d54ccd7e50afdf294dff659da913b779e4ecaaa64917b4f561bb - Sigstore transparency entry: 2064451273
- Sigstore integration time:
-
Permalink:
victorlane/sanic-x402@263e39c33060e8103a0a0c4b065112e453e0618c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/victorlane
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@263e39c33060e8103a0a0c4b065112e453e0618c -
Trigger Event:
push
-
Statement type: