> ## Documentation Index
> Fetch the complete documentation index at: https://docs.genlabs.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Pay an API from an agent with x402

> A TypeScript x402 server and buyer that run on the gen CLI alone, both halves settled on The DevNet Grid.

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](/reference/cli/install).
* 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](/quickstart/index) for how to obtain a token.

## Step 1: Point the CLI at DevNet

<Note>
  **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.
</Note>

```bash theme={null}
gen config env new --name devnet --rpc https://devnet.genlabs.co/rpc/
gen config env set devnet --header "authorization: Bearer <your-jwt>"
gen config env switch devnet
gen config show
```

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

```bash theme={null}
gen wallet create alice  --plaintext
gen wallet create server --plaintext

gen wallet faucet --wallet alice  --amount 1000000000
gen wallet faucet --wallet server --amount 1000

ALICE_ACCT=$(gen wallet --json show alice  --account-address-only | jq -r .result.account)
SERVER_ACCT=$(gen wallet --json show server --account-address-only | jq -r .result.account)

# build-activation's `destination` requires the recipient's
# 4-segment Holder component id, not the account. There's no
# direct CLI that prints it today; capture it from one tiny
# self-transfer (which echoes sender_component_id in --json).
SERVER_HOLDER=$(gen wallet --json transfer --wallet server \
  --to "$SERVER_ACCT" --amount 1 --yes \
  | jq -r .result.sender_component_id)

echo "ALICE_ACCT=$ALICE_ACCT"
echo "SERVER_ACCT=$SERVER_ACCT"
echo "SERVER_HOLDER=$SERVER_HOLDER"
```

> **`--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:

```ts theme={null}
import { spawnSync } from "node:child_process";
import { createServer } from "node:http";
import { createHash } from "node:crypto";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const PORT     = Number(process.env.PORT ?? 4021);
const PAY_TO   = process.env.PAY_TO;
const PRICE    = process.env.PRICE_SUBUNITS ?? "1000";
const NETWORK  = "gen-devnet-1";
const SCHEME   = "gen-exact";

if (!PAY_TO) {
  console.error("set PAY_TO to the merchant wallet's grd@... account locator");
  process.exit(1);
}

function gen(args: string[]) {
  const [group, ...rest] = args;
  const r = spawnSync("gen", [group, "--json", ...rest], { encoding: "utf8" });
  const j = JSON.parse(r.stdout);
  if (j.ok === false) throw new Error(`gen ${args.join(" ")}: ${j.error}`);
  return j.result;
}

const seen = new Map<string, number>();
const SEEN_TTL_MS = 5 * 60 * 1000;

function challenge(resource: string) {
  return {
    x402Version: 1,
    error: "X-PAYMENT header is required",
    accepts: [{
      scheme: SCHEME, network: NETWORK, asset: "GEN",
      payTo: PAY_TO, maxAmountRequired: PRICE,
      resource, mimeType: "application/json",
      maxTimeoutSeconds: 30, extra: {},
    }],
  };
}

async function settle(headerB64: string): Promise<string> {
  let envelope: any;
  try {
    envelope = JSON.parse(Buffer.from(headerB64, "base64").toString("utf8"));
  } catch {
    throw { code: 402, body: { x402Version: 1, error: "X-PAYMENT malformed" } };
  }
  if (envelope.scheme !== SCHEME || envelope.network !== NETWORK) {
    throw { code: 402, body: { x402Version: 1, error: "unsupported scheme or network" } };
  }
  const signedB64 = envelope.payload?.signed_activation;
  if (!signedB64) throw { code: 402, body: { x402Version: 1, error: "X-PAYMENT malformed" } };

  const bytes = Buffer.from(signedB64, "base64");
  const hash  = createHash("sha256").update(bytes).digest("hex");

  const now = Date.now();
  for (const [k, t] of seen) if (now - t > SEEN_TTL_MS) seen.delete(k);
  if (seen.has(hash)) throw { code: 402, body: { x402Version: 1, error: "payment already used" } };
  seen.set(hash, now);

  // gen client submit-activation reads signed bytes from a file, not stdin or argv.
  const dir = mkdtempSync(join(tmpdir(), "x402-"));
  const file = join(dir, "signed.txt");
  writeFileSync(file, "0x" + bytes.toString("hex"));
  let activationId: string;
  try {
    activationId = gen(["client", "submit-activation", "--signed-activation-file", file]).activation_id;
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }

  // Poll until the chain finalizes.
  const deadline = Date.now() + 30_000;
  while (Date.now() < deadline) {
    const got = gen(["client", "get-activation", "--activation-id", activationId]);
    const status = got?.activation?.status;
    if (status === "success") return activationId;
    if (status === "failed")  throw { code: 402, body: { x402Version: 1, error: "settlement failed", activation_id: activationId } };
    await new Promise((r) => setTimeout(r, 200));
  }
  throw { code: 402, body: { x402Version: 1, error: "settlement timed out", activation_id: activationId } };
}

createServer(async (req, res) => {
  if (!req.url?.startsWith("/api/quote/")) { res.writeHead(404).end(); return; }
  const symbol = decodeURIComponent(req.url.slice("/api/quote/".length));
  const header = req.headers["x-payment"];
  if (typeof header !== "string") {
    res.writeHead(402, { "content-type": "application/json" })
       .end(JSON.stringify(challenge(req.url)));
    return;
  }
  try {
    const tx = await settle(header);
    const responseHeader = Buffer.from(JSON.stringify({
      success: true, transaction: tx, network: NETWORK,
    }), "utf8").toString("base64");
    res.writeHead(200, {
      "content-type": "application/json",
      "x-payment-response": responseHeader,
    }).end(JSON.stringify({ symbol, price: 1234.56, paid_via: tx }));
  } catch (e: any) {
    res.writeHead(e?.code ?? 500, { "content-type": "application/json" })
       .end(JSON.stringify(e?.body ?? { error: "internal" }));
  }
}).listen(PORT, () => console.log(`x402 server: http://127.0.0.1:${PORT} payTo=${PAY_TO}`));
```

## The buyer

Save this as `buyer.ts`. \~60 lines:

```ts theme={null}
import { spawnSync } from "node:child_process";

const WALLET  = "alice";
const NETWORK = "gen-devnet-1";

function gen(args: string[]) {
  const [group, ...rest] = args;
  const r = spawnSync("gen", [group, "--json", ...rest], { encoding: "utf8" });
  const j = JSON.parse(r.stdout);
  if (j.ok === false) throw new Error(`gen ${args.join(" ")}: ${j.error}`);
  return j.result;
}

const myAccount = gen(["wallet", "show", WALLET]).account;
let tag = (Date.now() & 0x3fffffff) >>> 0;

export async function payAndFetch(url: string): Promise<Response> {
  const probe = await fetch(url);
  if (probe.status !== 402) return probe;

  const { accepts } = (await probe.json()) as { accepts: any[] };
  const reqs = accepts.find((a) => a.scheme === "gen-exact" && a.network === NETWORK);
  if (!reqs) throw new Error(`no gen-exact requirement for ${NETWORK} in 402 response`);

  // The merchant's 4-segment payTo is
  //   <recipient-entity>,<token-code-id>,<token-contract>,<role-id>
  // Segment 2 (the third) is the token contract. That's all we need
  // to feed build-activation a 2-segment caller id; the chain
  // auto-routes (account, token) to the Holder.
  const tokenContract = reqs.payTo.split(",")[2];
  const built = gen([
    "wallet", "build-activation", WALLET,
    "--component-id", `${myAccount},${tokenContract}`,
    "--method", "transfer",
    "--params", JSON.stringify({ amount: [reqs.maxAmountRequired], destination: reqs.payTo }),
  ]);

  const signed = gen([
    "wallet", "sign-activation", WALLET,
    "--unsigned-activation", built.unsigned_activation_hex,
    "--activation-tag", String(tag++),
  ]).signed_activation_hex.replace(/^0x/, "");

  const envelope = {
    x402Version: 1, scheme: "gen-exact", network: NETWORK,
    payload: { signed_activation: Buffer.from(signed, "hex").toString("base64") },
  };
  const header = Buffer.from(JSON.stringify(envelope), "utf8").toString("base64");

  return await fetch(url, { headers: { "x-payment": header } });
}

// Run a few paid calls.
for (const sym of ["gen1", "gen2", "gen3"]) {
  const r = await payAndFetch(`http://127.0.0.1:4021/api/quote/${sym}`);
  const xpr = r.headers.get("x-payment-response");
  const tx  = xpr ? JSON.parse(Buffer.from(xpr, "base64").toString("utf8")).transaction : "(none)";
  console.log(sym, r.status, "tx=" + tx, await r.text());
}
```

## Run it

Two terminals. In the first, start the server:

```bash theme={null}
PAY_TO="$SERVER_HOLDER" node --experimental-strip-types server.ts
```

```
x402 server: http://127.0.0.1:4021 payTo=grd@1qgqqqqqgcte6a...
```

In the second, run the buyer:

```bash theme={null}
node --experimental-strip-types buyer.ts
```

Expected output:

```
gen1 200 tx=0x... { "symbol": "gen1", "price": 1234.56, "paid_via": "0x..." }
gen2 200 tx=0x... { ... }
gen3 200 tx=0x... { ... }
```

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:

| `error`                           | Cause                                                                              | Fix                                                                                                                                                                               |
| --------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"X-PAYMENT header is required"`  | First request, no header attached.                                                 | Loop into the build / sign / retry path.                                                                                                                                          |
| `"X-PAYMENT malformed"`           | Outer envelope is not valid base64-JSON.                                           | You base64-encoded the inner bytes instead of the envelope.                                                                                                                       |
| `"unsupported scheme or network"` | Your envelope's `scheme` or `network` does not match any `accepts[]` entry.        | Both must be exact string matches.                                                                                                                                                |
| `"payment already used"`          | Tag collision. The server hashes signed bytes into a seen-set with a 5-minute TTL. | Generate a fresh `--activation-tag`. A client restart that re-seeds from the wall clock avoids this.                                                                              |
| `"settlement failed"`             | The activation was submitted but the chain rejected it.                            | Wallet balance too low, or the signature was rejected. The failing `activation_id` is in the 402 response body; look it up with `gen client get-activation --activation-id <id>`. |
| `"settlement timed out"`          | Finalization exceeded 30 seconds.                                                  | Usually means the server's poll path is wrong, not that the chain is slow. Check `gen client get-activation` against the failing `activation_id` from the response body.          |

Anything that is not `402` is non-x402 and should be surfaced to the caller as-is.

## References

* [`gen wallet`](/reference/cli/wallet) for create / faucet / build-activation / sign-activation.
* [Sending transfers](/reference/cli/transfers) for the underlying submit / sign / get-activation surface.
* [Quickstart](/quickstart/index) for DevNet credentials and first-time setup.
* [x402.org](https://www.x402.org/) for the protocol spec.
