Skip to content

Commit 54ad22b

Browse files
committed
docs: add Demo 1 — Agentic joke buyer page (hq-tkzkr)
New page under the MUSD Payments with x402 sidebar group, alongside Overview and Quickstart. The Quickstart covers the browser-wallet flow (user clicks Pay now). Demo 1 covers the complementary flow: a headless Node client pays the same paywall with no UI and no human click. Content: - Intro contrasts the browser-buyer flow with the agentic flow and names the shared substrate (same protocol, same facilitator, same 0.001 MUSD charge). - Prereqs reuse Account A from the Quickstart instead of asking the reader to re-mint MUSD. Adds an up-front caution about pasting a private key into .env (testnet-only; production agentic flows use HSM/MPC/KMS signer). - Step 1 one-time Permit2 approval: cast call to read allowance, cast send to grant uint256.max if zero. Aside notes non-Foundry alternatives (viem writeContract, any EVM wallet extension). - Step 2 install: @x402/fetch + @x402/evm + viem + dotenv, plus the pnpm.overrides block for the 2.10.x preview tarballs. - Step 3 client.ts: trimmed version of vativ/mezo-hack/apps/humor/ client/client.ts — loads private key, checks MUSD balance and Permit2 allowance, wraps fetch with wrapFetchWithPayment, GETs RESOURCE_URL, prints the joke plus the PAYMENT-RESPONSE header's settlement tx hash. Adds the same regex guard used in the Quickstart server.ts (0x + 64 hex for the private key, vs 0x + 40 hex for the receiving address). - .env snippet with RESOURCE_URL alternates for local seller vs humor.vativ.io and a security note to .gitignore .env. - Step 4 run + expected output. - 'How the auto-payment works' explainer: 5-step walk-through of the 402 -> sign permit -> retry with X-PAYMENT -> facilitator settles -> 200 + PAYMENT-RESPONSE receipt flow. - Troubleshooting table: missing Permit2 allowance, non-wrapped fetch, private-key format issues, empty 200, zero balance, insufficient allowance revert. - Security section: testnet-only key, .gitignore, swappable signer interface for production. - See also: Quickstart, humor repo, Permit2 upstream. MUSD casing normalized throughout (the upstream client.ts uses 'mUSD' in places; the quickstart convention for this group is 'MUSD' and this page follows that). 'x402' lowercase preserved. Nav: - astro.config.mjs: added the new slug to the 'MUSD Payments with x402' items array, after x402-quickstart. - SUMMARY.md: mirror the sidebar entry with the title 'Demo 1 — Agentic joke buyer'. Validation: dev server HTTP fetch returns 200 on /docs/developers/getting-started/musd-payments-x402/agentic-joke-buyer/ with the expected title ('Demo 1 — Agentic joke buyer | Mezo Documentation'), H1, and 12 H2s. Sidebar on the quickstart page now lists the new page 2,817 bytes after the 'MUSD Payments with x402' group label (peer of Overview and Quickstart). Pushed back on automation limits in hq-oqyhe still applies here: I did not add screenshots to this new page. If screenshots are wanted, the 'terminal showing successful run' and 'explorer view of the settlement tx' are two automatable candidates for a follow-up pass.
1 parent 2aa2742 commit 54ad22b

3 files changed

Lines changed: 332 additions & 1 deletion

File tree

astro.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,8 @@ export default defineConfig({
392392
collapsed: true,
393393
items: [
394394
'docs/developers/getting-started/musd-payments-x402',
395-
'docs/developers/getting-started/musd-payments-x402/x402-quickstart'
395+
'docs/developers/getting-started/musd-payments-x402/x402-quickstart',
396+
'docs/developers/getting-started/musd-payments-x402/agentic-joke-buyer'
396397
]
397398
},
398399
{

src/content/docs/docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,6 @@ topic: users
111111
* [MUSD Payments with x402](developers/getting-started/musd-payments-x402/index.mdx)
112112
* [Overview](developers/getting-started/musd-payments-x402/index.mdx)
113113
* [x402 Quickstart](developers/getting-started/musd-payments-x402/x402-quickstart.mdx)
114+
* [Demo 1 — Agentic joke buyer](developers/getting-started/musd-payments-x402/agentic-joke-buyer.mdx)
114115
* [chains](developers/chains/index.md)
115116
* [subgraphs](developers/subgraphs/index.md)
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
---
2+
title: Demo 1 — Agentic joke buyer
3+
description: >-
4+
A headless Node client that pays the Mezo x402 joke paywall
5+
autonomously using @x402/fetch and a funded testnet wallet — no
6+
browser, no Connect wallet button, no human click.
7+
topic: developers
8+
---
9+
10+
import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
11+
12+
The [Quickstart](./x402-quickstart/) walked through a **buyer-clicks-the-button**
13+
flow: the paywall renders in a browser, the reader signs with MetaMask,
14+
the joke appears.
15+
16+
Demo 1 flips that around: a **headless agent** pays the same paywall,
17+
with no UI and no human in the loop. The agent loads a private key,
18+
wraps `fetch()` with x402 payment handling, makes one GET request,
19+
and the `@x402/fetch` helper does the rest — detect `402`, sign a
20+
permit2 authorization, retry, read the receipt header, print the joke.
21+
22+
Same protocol, same facilitator, same `0.001 MUSD` charge. Different
23+
end user — code instead of a person.
24+
25+
## What you will build
26+
27+
A single-file Node client (`client.ts`) that:
28+
29+
1. Loads **Account A**'s private key (the account funded with MUSD in
30+
the [Quickstart](./x402-quickstart/#step-2-get-testnet-musd-to-pay-with)).
31+
2. Wraps global `fetch` with `wrapFetchWithPayment` from `@x402/fetch`.
32+
3. Calls a paywalled URL — either the live `humor.vativ.io/joke` or
33+
your own seller from Step 7 of the Quickstart.
34+
4. Prints the joke to the terminal plus the on-chain settlement
35+
transaction hash it paid.
36+
37+
No browser, no wallet extension, no Connect-wallet click.
38+
39+
<Aside type="caution" title="You will paste a private key into an env var">
40+
The agent signs with a raw private key loaded from `.env`. Use a
41+
**testnet-only, throwaway key** — never an address that holds real
42+
funds on any mainnet. For production agentic flows you'd replace the
43+
private key with a hardware signer, an MPC wallet, or a KMS-backed
44+
signer (see the x402 specs for the signer interface).
45+
</Aside>
46+
47+
## Prerequisites
48+
49+
- **Node.js 20+** and **pnpm 9+**. Same as the Quickstart.
50+
- **Account A (Buyer) from the Quickstart** — already funded with ≥
51+
1,800 MUSD and on Mezo Testnet. If you haven't done the Quickstart,
52+
do Steps 1 and 2 of it first, then come back here.
53+
- **Account A's private key.** In MetaMask:
54+
account menu → *"Account details"**"Show private key"* → type
55+
your MetaMask password. Copy the `0x…` private key somewhere you
56+
will delete after this demo.
57+
- A running x402 seller to call. Either:
58+
- Your local seller from Quickstart Step 7 (`localhost:3000/paid`),
59+
or
60+
- The live demo at `https://humor.vativ.io/joke` (no setup needed).
61+
62+
## Step 1: One-time Permit2 approval
63+
64+
The `@x402/fetch` EVM signer moves MUSD via
65+
[Uniswap's Permit2](https://github.com/Uniswap/permit2) — one allowance
66+
grant from your wallet to the Permit2 contract, then every subsequent
67+
payment is a signature, not a new on-chain approval. You only do this
68+
once per account.
69+
70+
Check whether Account A has already approved Permit2 for MUSD:
71+
72+
```bash
73+
cast call 0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503 \
74+
"allowance(address,address)(uint256)" \
75+
<account-A-address> \
76+
0x000000000022D473030F116dDEE9F6B43aC78BA3 \
77+
--rpc-url https://rpc.test.mezo.org
78+
```
79+
80+
(`0x0000…BA3` is the canonical Permit2 contract address, same on every
81+
EVM chain.)
82+
83+
If the returned allowance is `0`, approve it:
84+
85+
```bash
86+
cast send 0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503 \
87+
"approve(address,uint256)" \
88+
0x000000000022D473030F116dDEE9F6B43aC78BA3 \
89+
115792089237316195423570985008687907853269984665640564039457584007913129639935 \
90+
--rpc-url https://rpc.test.mezo.org \
91+
--private-key $CLIENT_PRIVATE_KEY
92+
```
93+
94+
(The big number is `type(uint256).max` — grant unlimited approval so
95+
you never have to re-approve.)
96+
97+
<Aside type="note" title="No cast? Use your preferred tool">
98+
`cast` is part of [Foundry](https://book.getfoundry.sh/). If you
99+
prefer Node, you can make the same `approve` call with `viem`'s
100+
`writeContract`, or run it through any EVM wallet extension by
101+
sending a token approval transaction to Permit2.
102+
</Aside>
103+
104+
## Step 2: Install client packages
105+
106+
```bash
107+
mkdir mezo-x402-joke-buyer && cd mezo-x402-joke-buyer
108+
pnpm init
109+
pnpm add @x402/fetch @x402/evm viem dotenv
110+
pnpm add -D typescript tsx @types/node
111+
```
112+
113+
The same Mezo-preview override reasoning from Quickstart Step 6
114+
applies here too: if `pnpm list @x402/fetch` shows `2.10.x` and not
115+
`2.11.0+`, add the overrides block to `package.json`:
116+
117+
```json
118+
"pnpm": {
119+
"overrides": {
120+
"@x402/evm": "https://github.com/vativ/x402-mezo-preview/releases/download/v2.10.0-mezo.6/x402-evm-2.10.0-mezo.6.tgz",
121+
"@x402/fetch": "https://github.com/vativ/x402-mezo-preview/releases/download/v2.10.0-mezo.6/x402-fetch-2.10.0-mezo.6.tgz"
122+
}
123+
}
124+
```
125+
126+
Then `pnpm install` again.
127+
128+
## Step 3: Write the agent
129+
130+
Create `client.ts`:
131+
132+
```typescript
133+
import 'dotenv/config';
134+
import { wrapFetchWithPayment, x402Client, decodePaymentResponseHeader } from '@x402/fetch';
135+
import { ExactEvmScheme } from '@x402/evm/exact/client';
136+
import { toClientEvmSigner, PERMIT2_ADDRESS } from '@x402/evm';
137+
import { createPublicClient, http, erc20Abi } from 'viem';
138+
import { privateKeyToAccount } from 'viem/accounts';
139+
import { mezoTestnet } from 'viem/chains';
140+
141+
const RESOURCE_URL = process.env.RESOURCE_URL || 'http://localhost:3000/paid';
142+
const RPC_URL = process.env.RPC_URL || 'https://rpc.test.mezo.org';
143+
const MUSD_ADDRESS = '0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503' as `0x${string}`;
144+
145+
const CLIENT_PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY as `0x${string}` | undefined;
146+
if (!CLIENT_PRIVATE_KEY || !/^0x[0-9a-fA-F]{64}$/.test(CLIENT_PRIVATE_KEY)) {
147+
throw new Error('CLIENT_PRIVATE_KEY must be set to a 0x-prefixed 64-hex-character private key.');
148+
}
149+
150+
async function main() {
151+
const account = privateKeyToAccount(CLIENT_PRIVATE_KEY);
152+
const publicClient = createPublicClient({ chain: mezoTestnet, transport: http(RPC_URL) });
153+
154+
// Sanity-check MUSD balance and Permit2 allowance before trying to pay.
155+
const balanceBefore = await publicClient.readContract({
156+
address: MUSD_ADDRESS, abi: erc20Abi, functionName: 'balanceOf', args: [account.address],
157+
});
158+
const allowance = await publicClient.readContract({
159+
address: MUSD_ADDRESS, abi: erc20Abi, functionName: 'allowance',
160+
args: [account.address, PERMIT2_ADDRESS as `0x${string}`],
161+
});
162+
console.log(`Buyer wallet: ${account.address}`);
163+
console.log(`MUSD balance: ${(Number(balanceBefore) / 1e18).toFixed(6)} MUSD`);
164+
console.log(`Permit2 allowance: ${allowance === 0n ? 'MISSING — run Step 1 first' : 'OK'}`);
165+
166+
// Wrap fetch with x402 payment handling: detects 402, signs, retries.
167+
const signer = toClientEvmSigner(account, publicClient);
168+
const client = new x402Client();
169+
client.register('eip155:*', new ExactEvmScheme(signer));
170+
const fetchWithPay = wrapFetchWithPayment(fetch, client);
171+
172+
console.log(`\nRequesting ${RESOURCE_URL} …`);
173+
const response = await fetchWithPay(RESOURCE_URL);
174+
if (!response.ok) {
175+
console.error(`Request failed: ${response.status} ${response.statusText}`);
176+
console.error(await response.text());
177+
return;
178+
}
179+
180+
const data = await response.json();
181+
console.log('\n=== Payment Successful ===');
182+
console.log(JSON.stringify(data, null, 2));
183+
184+
// Read the settlement receipt the server attached to the 200 response.
185+
const rxHeader = response.headers.get('PAYMENT-RESPONSE') || response.headers.get('X-PAYMENT-RESPONSE');
186+
if (rxHeader) {
187+
const rx = decodePaymentResponseHeader(rxHeader);
188+
console.log('\nOn-chain settlement:');
189+
console.log(` tx: ${rx.transaction}`);
190+
console.log(` network: ${rx.network}`);
191+
console.log(` success: ${rx.success}`);
192+
}
193+
194+
const balanceAfter = await publicClient.readContract({
195+
address: MUSD_ADDRESS, abi: erc20Abi, functionName: 'balanceOf', args: [account.address],
196+
});
197+
const delta = balanceBefore - balanceAfter;
198+
console.log(`\nMUSD spent: ${(Number(delta) / 1e18).toFixed(6)} MUSD`);
199+
}
200+
201+
main().catch((err) => { console.error(err); process.exit(1); });
202+
```
203+
204+
Create `.env`:
205+
206+
```bash
207+
# Account A's private key — use a testnet-only wallet. Never mainnet.
208+
CLIENT_PRIVATE_KEY=0xYOUR_ACCOUNT_A_PRIVATE_KEY_HERE
209+
210+
# Target URL. Pick one:
211+
# RESOURCE_URL=http://localhost:3000/paid # your local seller from the Quickstart
212+
# RESOURCE_URL=https://humor.vativ.io/joke # the live demo
213+
RESOURCE_URL=https://humor.vativ.io/joke
214+
```
215+
216+
Add `.env` to `.gitignore` immediately. Private key leakage is how
217+
test wallets drain.
218+
219+
## Step 4: Run the agent
220+
221+
```bash
222+
pnpm exec tsx client.ts
223+
```
224+
225+
Expected output:
226+
227+
```
228+
Buyer wallet: 0x<account-A>
229+
MUSD balance: 1800.000000 MUSD
230+
Permit2 allowance: OK
231+
232+
Requesting https://humor.vativ.io/joke …
233+
234+
=== Payment Successful ===
235+
{
236+
"setup": "Why did the Bitcoin go to therapy?",
237+
"punchline": "It had too many forks in its past."
238+
}
239+
240+
On-chain settlement:
241+
tx: 0x<tx-hash>
242+
network: eip155:31611
243+
success: true
244+
245+
MUSD spent: 0.001000 MUSD
246+
```
247+
248+
The `tx` hash is a real Mezo Testnet transaction — look it up on
249+
[`explorer.test.mezo.org`](https://explorer.test.mezo.org) to see the
250+
MUSD flow from Account A (the buyer) to the seller's `payTo` address,
251+
with gas paid by the facilitator.
252+
253+
## How the auto-payment works
254+
255+
`wrapFetchWithPayment(fetch, client)` returns a drop-in replacement for
256+
the global `fetch` that runs this sequence automatically on every call:
257+
258+
<Steps>
259+
260+
1. **First GET (unpaid).** The agent calls
261+
`fetchWithPay(RESOURCE_URL)`. The server responds `402 Payment
262+
Required` with a `PAYMENT-REQUIRED` header carrying a base64 JSON
263+
body describing the `accepts` list: scheme `exact`, asset
264+
(`0x1189…Ac503` = MUSD), amount (`1000000000000000` wei = `0.001
265+
MUSD`), `payTo` address, network (`eip155:31611`), and a
266+
`maxTimeoutSeconds` window.
267+
268+
2. **Select and sign.** The wrapped fetcher picks the `accepts` entry
269+
it can satisfy (EVM + MUSD), constructs a permit2
270+
`SignatureTransferDetails` for the amount and `payTo`, and the
271+
signer (`toClientEvmSigner`) signs it with Account A's private key.
272+
No on-chain transaction yet — just an off-chain signature.
273+
274+
3. **Retry with X-PAYMENT.** The fetcher resends the original GET with
275+
an `X-PAYMENT` header carrying the signed permit.
276+
277+
4. **Facilitator settles.** The server's `paymentMiddleware` forwards
278+
the signed permit to the facilitator at `facilitator.vativ.io`. The
279+
facilitator submits the on-chain `permitTransferFrom` to Permit2,
280+
which moves `0.001 MUSD` from Account A to the seller's `payTo`
281+
in a single transaction. The facilitator also pays gas — Account A
282+
never needs BTC for this.
283+
284+
5. **200 OK with receipt.** The server waits for settlement, then
285+
sends the real response body (the joke) with a `PAYMENT-RESPONSE`
286+
header containing the tx hash. `decodePaymentResponseHeader`
287+
decodes it for the agent's logs.
288+
289+
</Steps>
290+
291+
From the agent's perspective this is one `await fetchWithPay(url)`.
292+
Everything else is inside the library.
293+
294+
## Troubleshooting
295+
296+
| Symptom | Cause | Fix |
297+
|---|---|---|
298+
| `Permit2 allowance: MISSING` in the log | Account A has not approved Permit2 to spend its MUSD | Run the `cast send approve` command in Step 1 |
299+
| Request fails with `402` forever (never retries) | The agent is using plain `fetch` instead of `fetchWithPay` | Double-check the import; every call that should auto-pay must go through `fetchWithPay` |
300+
| `CLIENT_PRIVATE_KEY must be set…` | `.env` not loaded, wrong format, or key is `0x`-less | Confirm the key is `0x` + 64 hex characters; confirm `dotenv/config` is imported first |
301+
| Settlement succeeds but no joke body | Server responded `200` with an empty body, or the server is not the x402 demo | `curl -s -i <URL>` to confirm the server is up and returning JSON |
302+
| `MUSD balance: 0.000000` | Account A never received MUSD, or you pasted the wrong key | Check Quickstart Step 2; run `cast call balanceOf` to confirm balance on chain |
303+
| `insufficient allowance` revert on chain | Permit2 approval was less than the permit amount | Re-run Step 1 with the `type(uint256).max` argument shown |
304+
305+
## Security
306+
307+
- **Never put a real-funds private key in `.env`.** A testnet key that
308+
holds only borrowed MUSD and faucet BTC is fine for this demo.
309+
Anything else goes in a hardware signer or MPC wallet. Rotate the
310+
testnet key after the demo if you shared the repo anywhere.
311+
- **`.env` must be in `.gitignore`.** The default `pnpm init` does
312+
not create one — add it:
313+
```bash
314+
echo ".env" >> .gitignore
315+
```
316+
- The `toClientEvmSigner` interface is swappable — production agentic
317+
flows replace it with a signer backed by a KMS, HSM, or threshold
318+
wallet. The rest of the pipeline (`wrapFetchWithPayment`, the x402
319+
handshake, the facilitator) doesn't change.
320+
321+
## See also
322+
323+
- [Quickstart](./x402-quickstart/). Build the seller side of this flow
324+
and pay it with a browser wallet before pointing the agent at it.
325+
- [`vativ/mezo-hack/apps/humor`](https://github.com/vativ/mezo-hack/tree/main/apps/humor).
326+
Full source for the `humor.vativ.io` live demo — server + client
327+
both. This page's client is a trimmed version of `apps/humor/client/client.ts`.
328+
- [Uniswap Permit2](https://github.com/Uniswap/permit2). The allowance
329+
contract the EVM x402 scheme uses for token transfers.

0 commit comments

Comments
 (0)