SDKNode / TypeScriptSeller middleware

@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) and upto (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/sdk

Requires 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" in schemes without facilitatorAddress

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 list

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

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

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

First-request AgentagonSDKConfigError

Thrown on the first paid request that needs the seller’s endpoint set:

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

StatusWhenBuyer action
402 X-PAYMENT-REQUIREDNo X-PAYMENT header on a paid routeSign and retry
400X-PAYMENT header is malformedRe-encode
402 (re-issued)Facilitator returned success: falseRe-sign and retry
402 (re-issued)Facilitator returned 5xx or unreachableRetry later
503 Retry-After: 5No facilitator configured and AGENTAGON_ALLOW_LOCAL_ONLY not setConfigure a facilitator on the seller side
200Payment verified; response includes X-PAYMENT-RESPONSE with txHashDone

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