SDKPythonagentagon.seller

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) and upto (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 mpp and letting the gate front your origin
  • Embed payment middleware in a non-FastAPI app: use @agentagon/sdk/seller for Express / Hono, or wait for an ASGI-generic adapter

Install

pip install agentagon

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

ParameterRequiredDescription
appyesYour FastAPI/Starlette app instance
tokenyesService-scoped PAT (ag_pat_*)
priceyesUSD price per call as a string ("0.01")
pay_toyes (or env)Seller wallet address. Falls back to AGENTAGON_PAY_TO env
facilitator_urlyes (or env)Where the SDK posts X-PAYMENT for verification. Falls back to FACILITATOR_URL env
facilitator_addressonly if upto in schemesOn-chain Permit2 caller; required when upto is advertised
schemesnoDefaults to ["exact"]. Pass ["exact", "upto"] for both
endpoint_idsometimesRequired when the seller has 2+ registered endpoints (see Cookbook)
route_pricesnoPer-route price overrides. Empty string = free. See Cookbook
networknoCAIP-2 chain id; defaults to Base Sepolia
api_urlnoPlatform 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 AgentagonSDKConfigError

Raised 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 list

Advertising 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:

WhenFix
schemes contains anything other than "exact" or "upto"Drop the bad scheme
upto in schemes and facilitator_address is emptyPass facilitator_address=

First-request AgentagonSDKConfigError

Raised on the first paid request that needs to know the seller’s endpoint set:

WhenFix
Seller has 2+ endpoints and endpoint_id not passedPass 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:

StatusWhenBuyer action
402 X-PAYMENT-REQUIREDNo X-PAYMENT header on a paid routeSign and retry
400X-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 / unreachableRetry later
503 Retry-After: 5No facilitator configured and AGENTAGON_ALLOW_LOCAL_ONLY not setConfigure a facilitator on the seller side
200Payment verified; handler ran; response includes X-PAYMENT-RESPONSE with txHashDone

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= (or FACILITATOR_URL env) 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.