mayarin
SDKTypeScript

x402 gate

Sell an endpoint you host, per request — one middleware between your framework and your handler.

A resource registered under Agent endpoints prices a URL, but your server still answers every request for free. The gate is the part that closes it: a request with no payment is answered with Mayarin’s price in the standard PAYMENT-REQUIRED header, a request carrying one is settled by Mayarin before your handler runs — a response cannot be un-served, so nothing runs before the charge.

The whole gate is two imports and one middleware:

import { createX402Gate } from "@mayarin/sdk";
import { x402Connect } from "@mayarin/sdk/x402/connect";

const gate = createX402Gate({
  baseUrl: "https://api.mayarin.xyz",
  resourceId: "your-resource-id",
});

app.get("/premium", x402Connect(gate), paidHandler);

x402Connect is connect middleware over node:http types, so it runs under Express, Connect, a raw node:http server, or a Vite dev server — nothing in it is Express-specific, and the SDK carries no framework dependency.

Register the resource

The only step that needs the secret key, and the only place it is ever used — the request-time surface is keyless:

import { createMayarin } from "@mayarin/sdk";

const mayarin = createMayarin({
  baseUrl: "https://api.mayarin.xyz",
  secretKey: "sk_",
});

await mayarin.x402.resources.register({
  id: "your-resource-id",
  url: "https://your.server/premium", // must byte-match what an agent calls
  price: { amount: "2000", asset: "USDC" },
  maxTimeoutSeconds: 3600,
  accepts: [
    { chain: "base-sepolia", asset: "USDC", contract: "0x…", payTo: "0x…" },
  ],
  listed: true,
});

mayarin.x402.resources also lists (list), removes (remove), and opts in or out of the public index (listInIndex / unlistFromIndex). The token’s EIP-712 domain and transfer method are read off the contracts at registration, never entered.

What the middleware does

RequestAnswer
No PAYMENT-SIGNATURE header402 with PAYMENT-REQUIRED (base64 of the price JSON), empty body
Structurally bad signature402 with a fresh price and the reason on its error field
A signature Mayarin refuses402 with a fresh price and the refusal reason on its error field
A settled paymentYour handler runs once, with PAYMENT-RESPONSE set on the answer
The same signature againThe recorded response, verbatim — no second charge, no second handler run
Mayarin unreachable502 — the handler never runs. Paid content is not served on Mayarin’s say-so being missing

The happy path is one settle with no verify round-trip: Mayarin rebuilds the payment requirements from the registered resource and reads the transfer back off the chain, so verification is part of settling. verify exists for a payer’s separate “would this go through?” question.

Failures of Mayarin itself throw MayarinX402Error with a codePRICE_UNAVAILABLE or SETTLE_UNAVAILABLE, both retryable — which the middleware answers with 502. Fail-closed is the rule: a merchant does not serve paid content when the settlement layer cannot be reached.

Delivery and replay policy

One authorization buys one execution. A payer whose HTTP response was lost on the way back retries with the identical signature, and the gate re-serves the recorded response — keyed by the payment’s idempotency key (its nonce), expiring at the authorization’s validBefore, the whole window in which a retry is legal. Your handler’s own side effects are your idempotency to own: the gate guarantees one execution per authorization, not one execution per retry.

The store is in memory, per process, bounded by live payments. After a restart it is empty: a replayed signature reaches Mayarin, which refuses the spent nonce fail-closed — still no second charge, but no re-serve either.

The recorded response is re-served in full, so gate content that streams or answers range requests.

Inject fetch and now for tests, and replays for a store of your own:

const gate = createX402Gate({ baseUrl, resourceId, fetch, now, replays });

A runnable example

apps/x402-merchant in the repo is this page as a working server: register, serve, and prove unpaid / paid / replayed with the repo’s scripts/e2e-x402.ts, including --repeat 2 for the delivery policy.