Skip to main content

x402 payment-gated routes for Sanic: ask for payment with a single decorator

Project description

sanic-x402

License: MIT Python 3.10+ x402 Protocol

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

  1. A client requests a gated route without payment.
  2. sanic-x402 responds 402 with a challenge (JSON for agents, an HTML paywall for browsers) describing price, asset, network, and receiver.
  3. The client signs a payment authorization and retries with a PAYMENT-SIGNATURE header.
  4. The payment is verified with the facilitator, then your handler runs.
  5. On a successful response the payment settles on-chain and the receipt is attached as a PAYMENT-RESPONSE header.

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 402 with 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-handler await request.respond() triggers settlement at respond() time, so prefer returned responses on paid routes.
  • Websockets are not gated. A warning is logged if @paid is applied to one.
  • Facilitator sync is lazy. Capabilities are fetched on the first protected request; pass sync_facilitator_on_start=False to disable.

Development

uv sync
uv run pytest

Tests run fully offline against a fake facilitator.

Resources

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sanic_x402-0.1.0.tar.gz (184.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sanic_x402-0.1.0-py3-none-any.whl (15.2 kB view details)

Uploaded Python 3

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

Hashes for sanic_x402-0.1.0.tar.gz
Algorithm Hash digest
SHA256 663b942ae922757065162c2255dd44d2bd8e0ac8ad398acd18eae4e0996d9224
MD5 8dbfb1ddc2b08b54a5e7edcc6d3746e9
BLAKE2b-256 20559336cd3c210030d3385c9d6dba98d78bc0ab8d38db0be4bc6eaf1d1a67d8

See more details on using hashes here.

Provenance

The following attestation bundles were made for sanic_x402-0.1.0.tar.gz:

Publisher: release.yml on victorlane/sanic-x402

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for sanic_x402-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 96e6dd80b514d54ccd7e50afdf294dff659da913b779e4ecaaa64917b4f561bb
MD5 34e53cb4408b71be53e2364b9cf058cf
BLAKE2b-256 9663a967e85946f4a9e7b0f847bdd73d7f92eaa66d4c7fddd1ea6a161fd76474

See more details on using hashes here.

Provenance

The following attestation bundles were made for sanic_x402-0.1.0-py3-none-any.whl:

Publisher: release.yml on victorlane/sanic-x402

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page