@agentagon/sdk/seller
Express / Hono / Node http middleware that gates endpoints behind x402 micropayments. Mirror of agentagon.seller (Python) with byte-identical wire format; a buyer cannot tell which language the seller is running.
Overview
Use @agentagon/sdk/seller when you want to:
- Charge per-call USDC for an Express or Hono service
- Get paid in USDC on Base or Solana without running your own wallet
- Have ledger rows attributed to the right service and endpoint
- Optionally advertise both
exact(fixed-price EIP-3009) andupto(variable-price Permit2) schemes - Capture content hashes for response quality scoring (
installBodyCapture)
It’s not the right tool when you want to:
- Run an MPP (Managed Payment Proxy) seller: use the gate’s managed-proxy mode (
agentagon service init --protocol mpp) - Embed in a Python FastAPI app: use
agentagon.seller
Install
npm install @agentagon/sdkRequires Node.js 18+ (native fetch).
Quickstart
The five-line embed (Express):
import express from "express";
import { wrap } from "@agentagon/sdk/seller";
const app = express();
app.use(wrap({
token: process.env.AGENTAGON_PAT!,
price: "0.01",
payTo: "0xYourSellerWallet",
facilitatorUrl: "https://x402.org/facilitator",
}));
app.get("/data", (_req, res) => res.json({ data: "ok" }));
app.listen(3000);Hono works the same shape; the middleware detects the framework automatically:
import { Hono } from "hono";
import { wrap } from "@agentagon/sdk/seller";
const app = new Hono();
app.use("*", wrap({ token: process.env.AGENTAGON_PAT!, price: "0.01" }));
app.get("/data", (c) => c.json({ data: "ok" }));API reference
wrap
function wrap(opts: WrapOptions): Middleware
interface WrapOptions {
token: string; // service-scoped PAT (ag_pat_*)
price: string; // USD per call as a string ("0.01")
payTo?: string; // seller wallet (or AGENTAGON_PAY_TO env)
facilitatorUrl?: string; // (or FACILITATOR_URL env)
facilitatorAddress?: string; // required when "upto" in schemes
schemes?: string[]; // default ["exact"]
endpointId?: string; // required if seller has 2+ endpoints
routePrices?: Record<string, string>;
network?: string; // CAIP-2 chain id; default "eip155:84532"
apiUrl?: string; // platform base url for ledger reporting
captureBody?: boolean; // default true; set false to opt out
}token is a service-scoped PAT (ag_pat_*). Mint one with agentagon service pat create. Master keys (ag_live_sk_*) are no longer accepted.
The returned Middleware works with the standard (req, res, next) signature; framework adapters detect Express vs Hono vs raw http.
Throws AgentagonSDKConfigError synchronously for misconfigurations:
- Invalid scheme (anything other than
"exact"or"upto") "upto"inschemeswithoutfacilitatorAddress
Multi-endpoint pinning errors throw AgentagonSDKConfigError on the first paid request, not at construction, because the platform’s endpoint set is fetched lazily.
AgentagonSDKConfigError
import { AgentagonSDKConfigError } from "@agentagon/sdk/seller";Thrown for both construction-time misconfigs and the first-request multi-endpoint case. The error name is "AgentagonSDKConfigError" so you can catch by instanceof or by .name. The message lists every known (slug, endpointId) pair when the cause is multi-endpoint.
installBodyCapture
import { installBodyCapture } from "@agentagon/sdk/seller";
app.use(installBodyCapture());
app.use(wrap({ token: "...", price: "0.01" }));Wraps res.write / res.end / res.send to accumulate response bytes and compute a sha256 hash on finish. The hash lands in the ledger POST as content_hash, giving the platform a stable signal for response quality scoring.
captureBody: false in WrapOptions opts out when a downstream middleware manages the response stream in a way the wrapper can’t see. Most callers should leave it on (it’s the default).
Cookbook
Per-route pricing
app.use(wrap({
token: "ag_pat_...",
price: "0.01", // default
routePrices: {
"/api/cheap": "0.001",
"/api/expensive": "0.10",
"/health": "0", // free
},
}));routePrices overrides price per exact path match. Use "0" to mark a route free; the middleware skips the 402 and the request goes straight to your handler.
Multi-endpoint pinning
If your account has 2+ registered endpoints, pin endpointId so ledger rows are attributed correctly:
app.use(wrap({
token: "ag_pat_...",
price: "0.01",
endpointId: "ep_abc123",
}));If endpointId is omitted and the platform reports exactly one endpoint, the SDK auto-picks it. Two or more endpoints without an explicit endpointId throws AgentagonSDKConfigError on the first paid request.
Find your endpoint ids with:
agentagon endpoints listAdvertising both exact and upto schemes
app.use(wrap({
token: "ag_pat_...",
price: "0.05",
payTo: "0xYourWallet",
facilitatorUrl: "https://x402.org/facilitator",
schemes: ["exact", "upto"],
facilitatorAddress: "0xd407e409E34E0b9afb99EcCeb609bDbcD5e7f1bf",
}));facilitatorAddress is required when upto is in schemes. It’s the on-chain address that calls Permit2; buyers bind their Permit2 witness to it. Without it, wrap() throws AgentagonSDKConfigError at construction.
Buyers paying with upto need a one-time on-chain Permit2 allowance. See Troubleshooting.
Local dev (no facilitator)
process.env.AGENTAGON_ALLOW_LOCAL_ONLY = "1";
app.use(wrap({ 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).
Middleware order
Install wrap before your routes, and before authentication middleware:
app.use(wrap({ token: "...", price: "0.01" })); // payment first
app.use(authMiddleware); // auth second
app.get("/data", handler); // routes lastUnpaid requests short-circuit at the wrap layer; auth never runs for them. This avoids leaking auth signals to anonymous probers.
Inspect what the 402 looks like
import express from "express";
import { wrap } from "@agentagon/sdk/seller";
const app = express();
app.use(wrap({ token: "ag_pat_...", price: "0.01", payTo: "0xseller" }));
app.get("/anything", (_req, res) => res.json({}));
const server = app.listen(0, async () => {
const port = (server.address() as any).port;
const resp = await fetch(`http://127.0.0.1:${port}/anything`);
console.log(resp.status); // 402
const challenge = JSON.parse(
Buffer.from(resp.headers.get("X-PAYMENT-REQUIRED")!, "base64").toString(),
);
console.log(challenge);
server.close();
});The challenge body shows exactly what buyers see: accepts[] array with scheme, network, payTo, amount, asset, and extra (EIP-712 domain plus facilitatorAddress for upto).
TypeScript types are first-class
import type { WrapOptions } from "@agentagon/sdk/seller";
const config: WrapOptions = {
token: process.env.AGENTAGON_PAT!,
price: "0.01",
payTo: process.env.PAY_TO!,
facilitatorUrl: process.env.FACILITATOR_URL!,
};
app.use(wrap(config));Both WrapOptions and the error class export from the same /seller subpath.
Errors
Construction-time AgentagonSDKConfigError
Thrown 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 facilitatorAddress is empty | Pass facilitatorAddress |
First-request AgentagonSDKConfigError
Thrown on the first paid request that needs the seller’s endpoint set:
| When | Fix |
|---|---|
Seller has 2+ endpoints and endpointId not passed | Pass endpointId matching one of the printed (slug, id) pairs |
The full error message lists every endpoint the platform reports for your seller.
Runtime HTTP responses
These go out to the buyer; they are not thrown from your handler:
| 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 | Re-encode |
402 (re-issued) | Facilitator returned success: false | Re-sign and retry |
402 (re-issued) | Facilitator returned 5xx or 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; response includes X-PAYMENT-RESPONSE with txHash | Done |
The middleware retries facilitator calls 3 times with exponential backoff for transient 5xx and timeouts; only after exhaustion does it return a fresh 402 to the buyer.
Troubleshooting
AgentagonSDKConfigError: facilitatorAddress is required
You passed schemes: ["exact", "upto"] without facilitatorAddress. The upto scheme uses Permit2; buyers’ witnesses bind to a specific on-chain caller. Get the address 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, endpointId) pair. Pick one and pass it:
app.use(wrap({ token: "...", price: "0.01", endpointId: "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, 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; it will not admit unverified payments silently.
Either:
- Set
facilitatorUrl(orFACILITATOR_URLenv), or - For local dev only, set
AGENTAGON_ALLOW_LOCAL_ONLY=1(never in production)
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 is dropped. Either move wrap closer to the routes (upstream of the conflicting middleware), or pass captureBody: false and accept that content_hash won’t be reported either.
Hono context vs Express request shape
The wrapper detects which framework you’re using via duck-typing on the request/response objects. If you wrap a custom server that doesn’t match Express or Hono shapes, payment headers won’t be set correctly. Open an issue with a minimal repro; we can add adapters.
Cross-language parity (Python vs Node sellers)
The wire format is byte-identical between agentagon.seller and @agentagon/sdk/seller. A buyer that pays a Python seller can pay a Node seller with the same code. The CI test tests/test_signing_determinism.py boots both SDKs against the same buyer and diffs the headers; if you see a discrepancy, file it as a regression.