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.
Metadata
Release files for sanic-x402 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| sanic_x402-0.1.0.tar.gz | 184.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sanic_x402-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 199.4 kB
Release files / sanic_x402-0.1.0.tar.gz
| Download URL | sanic_x402-0.1.0.tar.gz |
|---|---|
| Size | 184.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
663b942ae922757065162c2255dd44d2bd8e0ac8ad398acd18eae4e0996d9224
|
|
BLAKE2b-256 checksum How to use checksums |
20559336cd3c210030d3385c9d6dba98d78bc0ab8d38db0be4bc6eaf1d1a67d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.
Transparency logRelease files / sanic_x402-0.1.0-py3-none-any.whl
| Download URL | sanic_x402-0.1.0-py3-none-any.whl |
|---|---|
| Size | 15.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
96e6dd80b514d54ccd7e50afdf294dff659da913b779e4ecaaa64917b4f561bb
|
|
BLAKE2b-256 checksum How to use checksums |
9663a967e85946f4a9e7b0f847bdd73d7f92eaa66d4c7fddd1ea6a161fd76474
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.
Transparency logSecurity for sanic-x402 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for sanic_x402-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.