Skip to main content
x402 attaches per-request payment to HTTP. A server quotes a price in 402 Payment Required, the client signs a transfer and retries with an X-PAYMENT header, the server settles on chain before returning the resource. This page gets you both halves of that flow as two small TypeScript files, running on The DevNet Grid, using nothing but the gen CLI and Node. You’ll run the server on 127.0.0.1:4021 and the buyer in a second terminal. Both talk to DevNet through the same CLI configuration.

Prerequisites

  • The gen CLI on your PATH. See Install the CLI.
  • Node 22.6 or later (for --experimental-strip-types). Node 20 works if you swap node --experimental-strip-types <file>.ts for npx tsx <file>.ts.
  • DevNet credentials (RPC URL and JWT). If you don’t have one yet, see Quickstart for how to obtain a token.

Step 1: Point the CLI at DevNet

DevNet access. Ask your Gen Labs contact for the RPC URL and a bearer token; substitute them for https://devnet.genlabs.co/rpc/ and <your-jwt> below.
gen config show should list a devnet environment marked active (*) with rpc_url: "https://devnet.genlabs.co/rpc/" and a redacted authorization header. Both the server and the buyer below shell out to the same gen binary, so this setup applies to both.

Step 2: Create and fund two wallets

The buyer (alice) signs and spends. The merchant (server) receives. The faucet creates each account on first use and funds it; nothing else needs bootstrapping.
--plaintext stores the private key unencrypted on disk. Fine for automation accounts that hold operating funds only.
ALICE_ACCT is the buyer’s account; the buyer signs from this wallet. SERVER_HOLDER is the merchant’s four-segment Holder component id for the GEN token contract. That’s what gets advertised as payTo, because build-activation’s destination parameter requires a Holder, not an account. SERVER_ACCT is only used to derive SERVER_HOLDER above; you can drop it after capture.

The server

Save this as server.ts. ~100 lines, Node 20+, no dependencies:

The buyer

Save this as buyer.ts. ~60 lines:

Run it

Two terminals. In the first, start the server:
In the second, run the buyer:
Expected output:
Three paid calls, three on-chain transfers visible in the server logs.

Walking through it

The server

challenge() builds the 402 body once per request. It advertises scheme: "gen-exact" and network: "gen-devnet-1"; the label is just a name both halves agree on, so pick anything you want as long as the buyer matches. payTo is the merchant’s bech32m account locator: the same string the reader saw printed from gen wallet show. settle() is the verify-and-submit pipeline: base64-decode the envelope, check the scheme and network match, SHA-256 the signed bytes against an in-memory dedup map (5-minute TTL), submit the activation through gen client submit-activation --signed-activation-file, then poll gen client get-activation until the chain reports status: "success" or failed. The poll loop has a 30 s ceiling; on DevNet, success usually arrives in well under a second. The seen-set lives in process memory. A server restart drops it. For a production gate, replace it with a backing store keyed on the signed-bytes hash.

The buyer

gen() is a thin shell-out helper that calls gen <group> --json <subcommand> ... and parses the result. Every wallet operation goes through it. The pay-then-fetch loop: probe the URL, parse the accepts[] array, pick the entry that advertises your scheme and network, build the unsigned activation with gen wallet build-activation, sign with gen wallet sign-activation and a fresh --activation-tag, wrap the result in the x402 envelope, retry with X-PAYMENT set. Two details that matter:
  • --activation-tag is a u32 mixed into the signing pre-image. Two activations with identical fields and the same tag hash to the same bytes; the server’s seen-set rejects the second one as a replay. The buyer seeds from Date.now() & 0x3fffffff and increments per call.
  • The inner signed_activation is base64 of the raw signed bytes, not the hex string. The buyer’s encode chain is hex → bytes → base64, then envelope JSON → base64.

When the server says no

Every payment-side failure comes back as another 402 with a specific error string. The ones worth handling explicitly: Anything that is not 402 is non-x402 and should be surfaced to the caller as-is.

References

  • gen wallet for create / faucet / build-activation / sign-activation.
  • Sending transfers for the underlying submit / sign / get-activation surface.
  • Quickstart for DevNet credentials and first-time setup.
  • x402.org for the protocol spec.