agentagon.seller
FastAPI middleware that gates endpoints behind x402 micropayments. Wrap your app in 5 lines; the middleware handles the 402 handshake, facilitator verification, content-hash capture, and best-effort ledger reporting.
Overview
agentagon.seller.wrap is the right tool when you want to:
- Charge per-call USDC for a FastAPI service you already have
- Get paid in USDC on Base or Solana without running your own wallet
- Have ledger rows attributed to the right service and endpoint without writing your own bookkeeping
- Optionally advertise both
exact(fixed-price EIP-3009) andupto(variable-price Permit2) payment schemes
It’s not the right tool when you want to:
- Charge per-call but settle in another currency: x402 is USDC-only today
- Run an MPP (Managed Payment Proxy) seller that fronts an LLM-routed service: use the managed proxy mode by registering with
agentagon service init --protocol mppand letting the gate front your origin - Embed payment middleware in a non-FastAPI app: use
@agentagon/sdk/sellerfor Express / Hono, or wait for an ASGI-generic adapter
Install
pip install agentagonThe seller middleware needs FastAPI (or any Starlette-based framework) at runtime. pip install agentagon pulls FastAPI in transitively.
Quickstart
The five-line embed:
from fastapi import FastAPI
from agentagon.seller import wrap
app = FastAPI()
@app.get("/data")
def data():
return {"data": "ok"}
app = wrap(
app,
token="ag_pat_...", # service-scoped PAT
price="0.01", # USDC per call
pay_to="0xYourSellerWallet",
facilitator_url="https://x402.org/facilitator",
)Now every request to /data without an X-PAYMENT header gets a 402 + X-PAYMENT-REQUIRED response. Buyers sign an EIP-3009 authorization for $0.01 USDC, base64-encode it into X-PAYMENT, and retry. The middleware verifies the signature with the facilitator (POST /settle), and on success the request hits your handler.
token is a service-scoped PAT (ag_pat_*). Mint one with agentagon service pat create or in the dashboard. Master keys (ag_live_sk_*) were hard-cut on 2026-04-27 and are no longer accepted.
API reference
wrap
def wrap(
app,
*,
token: str,
price: str,
pay_to: str = "",
facilitator_url: str = "",
facilitator_address: str = "",
schemes: list[str] | None = None,
endpoint_id: str = "",
route_prices: dict[str, str] | None = None,
network: str = "eip155:84532",
api_url: str = "",
) -> FastAPI:Wraps app in middleware that gates every route except /health behind payment. Returns the wrapped app; assign to the same name (app = wrap(app, ...)) so subsequent route registrations still hit the wrapped instance.
| Parameter | Required | Description |
|---|---|---|
app | yes | Your FastAPI/Starlette app instance |
token | yes | Service-scoped PAT (ag_pat_*) |
price | yes | USD price per call as a string ("0.01") |
pay_to | yes (or env) | Seller wallet address. Falls back to AGENTAGON_PAY_TO env |
facilitator_url | yes (or env) | Where the SDK posts X-PAYMENT for verification. Falls back to FACILITATOR_URL env |
facilitator_address | only if upto in schemes | On-chain Permit2 caller; required when upto is advertised |
schemes | no | Defaults to ["exact"]. Pass ["exact", "upto"] for both |
endpoint_id | sometimes | Required when the seller has 2+ registered endpoints (see Cookbook) |
route_prices | no | Per-route price overrides. Empty string = free. See Cookbook |
network | no | CAIP-2 chain id; defaults to Base Sepolia |
api_url | no | Platform base URL for ledger reporting. Defaults to AGENTAGON_API_URL env or https://api.agentagon.ai |
Raises ValueError at construction time for misconfigurations (e.g. upto in schemes without facilitator_address). Multi-endpoint pinning errors raise AgentagonSDKConfigError on the first paid request, not at wrap time, because endpoint discovery requires a network round trip to the platform.
AgentagonSDKConfigError
from agentagon.seller import AgentagonSDKConfigErrorRaised when the seller has 2+ registered endpoints and endpoint_id was not pinned. The exception message lists every known (slug, endpoint_id) pair so you can pick one and pass it. Inherits from RuntimeError.
This is a runtime error rather than a wrap-time error because the platform is the source of truth on how many endpoints a seller has, and that requires a POST /v1/auth/verify call which the middleware defers until the first paid request.
Cookbook
Per-route pricing
app = wrap(
app,
token="ag_pat_...",
price="0.01", # default
pay_to="0xYourWallet",
facilitator_url="https://x402.org/facilitator",
route_prices={
"/api/cheap": "0.001",
"/api/expensive": "0.10",
"/health": "0", # free
},
)route_prices overrides price per exact path match. Use "0" to mark a route free; the middleware skips the 402 dance entirely and the request goes straight to the handler.
Multi-endpoint pinning
If your account has 2+ registered endpoints (different paths, different prices, different capabilities), pin endpoint_id so ledger rows are attributed correctly:
app = wrap(
app,
token="ag_pat_...",
price="0.01",
pay_to="0xYourWallet",
facilitator_url="https://x402.org/facilitator",
endpoint_id="ep_abc123",
)If endpoint_id is omitted and the platform reports exactly one endpoint for the seller, the SDK auto-picks it. Two or more endpoints without an explicit endpoint_id raises AgentagonSDKConfigError on the first paid request rather than mis-attributing revenue.
You can find your endpoint ids with:
agentagon endpoints listAdvertising both exact and upto schemes
app = wrap(
app,
token="ag_pat_...",
price="0.05",
pay_to="0xYourWallet",
facilitator_url="https://x402.org/facilitator",
schemes=["exact", "upto"],
facilitator_address="0xd407e409E34E0b9afb99EcCeb609bDbcD5e7f1bf",
)facilitator_address is required when upto is in schemes. It’s the on-chain address that calls Permit2; buyers bind their Permit2 witness to it. Discoverable via the facilitator’s GET /supported.kinds[].extra.facilitatorAddress. Without it, wrap() raises ValueError at construction.
Buyers paying with upto need a one-time on-chain Permit2 allowance. See Troubleshooting below.
Local dev (no facilitator)
For local development without a facilitator running:
import os
os.environ["AGENTAGON_ALLOW_LOCAL_ONLY"] = "1"
app = wrap(app, token="ag_pat_...", price="0.01")In this mode the middleware accepts well-formed X-PAYMENT headers without facilitator verification. Never enable in production. Without the opt-in, the middleware refuses to serve gated requests when no facilitator is configured (returns 503 with Retry-After: 5).
Inspect what the 402 looks like
import base64, json, httpx
from fastapi import FastAPI
from agentagon.seller import wrap
app = wrap(FastAPI(), token="ag_pat_...", price="0.01", pay_to="0xseller")
client = httpx.Client(transport=httpx.ASGITransport(app=app), base_url="http://test")
resp = client.get("/anything")
assert resp.status_code == 402
challenge = json.loads(base64.b64decode(resp.headers["X-PAYMENT-REQUIRED"]))
print(json.dumps(challenge, indent=2))The challenge body shows exactly what buyers see: accepts[] array with scheme, network, payTo, amount, asset, and extra (EIP-712 domain name + version, plus facilitatorAddress for upto).
Capture content hashes for quality scoring
The middleware already captures content_hash = sha256(response body) and includes it in the ledger POST. No code needed; this is on by default. The hash gives the platform a stable signal for response quality scoring without inspecting body content.
To opt out (e.g. when a downstream middleware manages the response stream in a way the wrapper can’t see), set AGENTAGON_DISABLE_BODY_CAPTURE=1. Most callers should leave this on.
Read the active env at startup
import os
from agentagon.seller import wrap
app = wrap(
app,
token=os.environ["AGENTAGON_TOKEN"],
price=os.environ.get("AGENTAGON_PRICE", "0.01"),
pay_to=os.environ["AGENTAGON_PAY_TO"],
facilitator_url=os.environ["FACILITATOR_URL"],
)The middleware also reads AGENTAGON_PAY_TO, FACILITATOR_URL, and AGENTAGON_API_URL from env directly. Pass them explicitly when you want different values per app instance.
Errors
The middleware surfaces three classes of error.
Construction-time ValueError
Raised by wrap() itself when arguments are invalid:
| When | Fix |
|---|---|
schemes contains anything other than "exact" or "upto" | Drop the bad scheme |
upto in schemes and facilitator_address is empty | Pass facilitator_address= |
First-request AgentagonSDKConfigError
Raised on the first paid request that needs to know the seller’s endpoint set:
| When | Fix |
|---|---|
Seller has 2+ endpoints and endpoint_id not passed | Pass endpoint_id=... matching one of the printed (slug, id) pairs |
The full error message lists every endpoint the platform reports for your seller. You can match by slug.
Runtime HTTP responses
The middleware emits these to the buyer; they are not Python exceptions:
| Status | When | Buyer action |
|---|---|---|
402 X-PAYMENT-REQUIRED | No X-PAYMENT header on a paid route | Sign and retry |
400 | X-PAYMENT header is malformed (not base64 / not JSON) | Re-encode |
402 (re-issued) | Facilitator returned success:false (bad signature, etc.) | Re-sign and retry |
402 (re-issued) | Facilitator returned 5xx / unreachable | Retry later |
503 Retry-After: 5 | No facilitator configured and AGENTAGON_ALLOW_LOCAL_ONLY not set | Configure a facilitator on the seller side |
200 | Payment verified; handler ran; response includes X-PAYMENT-RESPONSE with txHash | Done |
Troubleshooting
ValueError: facilitator_address is required when 'upto' is in schemes
You passed schemes=["exact", "upto"] without facilitator_address. The upto scheme uses Permit2; buyers’ witnesses bind to a specific on-chain caller, so this is required. Get it from your facilitator’s GET /supported.kinds[].extra.facilitatorAddress.
AgentagonSDKConfigError on the first paid request
Your seller has 2+ registered endpoints. The error message lists each (slug, endpoint_id) pair. Pick one and pass it:
app = wrap(app, token="...", price="0.01", endpoint_id="ep_abc123", ...)If you only have one endpoint and still see this error, the platform may have stale cache; check agentagon endpoints list to confirm.
Buyers see permit2_allowance_required
The upto scheme requires buyers to hold a one-time on-chain Permit2 allowance for USDC. Public testnet facilitators don’t yet ship gas-sponsoring extensions (see docs/EXTERNAL_GAPS.md EG-1), so each buyer wallet must approve once. Have the buyer run scripts/permit2_approve.py (in the agentagon repo) or call Permit2’s approve() directly. After that, future upto payments work without on-chain interaction.
If you don’t need upto, drop it from schemes and stay on exact (EIP-3009 is gas-sponsored by the facilitator).
503 with Retry-After: 5
The middleware is refusing to serve because no facilitator is configured and AGENTAGON_ALLOW_LOCAL_ONLY is not set. This is the hardened production stance; the middleware will not admit unverified payments silently.
Either:
- Set
facilitator_url=(orFACILITATOR_URLenv) to a real facilitator, or - For local dev only, set
AGENTAGON_ALLOW_LOCAL_ONLY=1(never in production)
PAT validation 401 right after issue
The middleware validates the PAT on first request and caches the result with a short cooldown. Newly minted PATs propagate within 5-10 seconds; if you see 401s longer than that, check agentagon whoami to confirm the token is still valid and has the right scope.
X-PAYMENT-RESPONSE header missing on 200
Some downstream middlewares replace res.send / res.write in a way the wrapper can’t intercept. The 200 still goes out, but the X-PAYMENT-RESPONSE header (with txHash, payer, etc.) is dropped. Move the wrap call closer to the routes (or upstream of the conflicting middleware), or report the conflict; we may add a defensive replay path.
Why isn’t there a wrap() for non-FastAPI Python frameworks?
wrap() is built around the ASGI protocol via Starlette, which FastAPI uses. If you have a Starlette app directly, the same wrap call works. For Flask/Django (WSGI), no adapter exists yet; track upstream interest and we’ll consider it.
For Express/Hono on Node, see @agentagon/sdk/seller.
What about MPP (Managed Payment Proxy)?
By design, this middleware is x402-only. MPP sellers should use the managed-proxy mode (onboard via agentagon service init --protocol mpp and let the gate front your origin). MPP needs HMAC challenge state plus session bookkeeping that doesn’t fit the per-request stateless shape of an embedded middleware.