Quickstart for x402
Esta página aún no está disponible en tu idioma.
This quickstart runs top to bottom. By the end, you will have paid
$0.001 in MUSD on Mezo Testnet against your local Express seller server using the
@x402/* SDKs.
For the conceptual context of what x402 is, start with the x402 Overview.
What you will build
Section titled “What you will build”A minimal two-part client/server app for purchasing a /paid API endpoint using MUSD:
- Merchant Server: an Express server with one protected route that returns JSON only after a valid MUSD payment settles.
- Buyer Client: a browser client that loads the bundled x402
paywallUI, connects a wallet, pays $0.001 MUSD on Mezo Testnet, and receives the protected API response.
Prerequisites
Section titled “Prerequisites”- Node.js 20+ and pnpm 9+ on your machine.
- A browser wallet extension that supports EVM networks (MetaMask, Rabby, Coinbase Wallet, or any wallet that exposes an EIP-1193 provider).
- ~20–30 minutes. Getting testnet MUSD involves a faucet drip + a borrow transaction; run those early (Steps 1–2) and they’ll be ready by the time you need them.
Step 1: Set up your wallet
Section titled “Step 1: Set up your wallet”1a. Create two accounts
Section titled “1a. Create two accounts”Open your wallet’s account switcher. Your first account is Account A (Buyer). Add a second account is Account B (Merchant) using whatever name helps you tell them apart. Ensure Account A (Buyer) is selected/active.
1b. Add Mezo Testnet as a network
Section titled “1b. Add Mezo Testnet as a network”With either account selected, open your wallet’s networks settings and add a custom network with these parameters:
| Field | Value |
|---|---|
| Network name | Mezo Testnet |
| Chain ID | 31611 |
| RPC URL | https://rpc.test.mezo.org |
| Block explorer | https://explorer.test.mezo.org |
| Currency symbol | BTC |
For mainnet parameters and alternative RPC providers, see Set Up Developer Environment.
Step 2: Get testnet MUSD to pay with
Section titled “Step 2: Get testnet MUSD to pay with”MUSD on testnet is minted, not faucet-dispensed, you borrow it against testnet BTC.
Select Account A (Buyer) in your wallet before starting allowing all of Step 2 funds going to Account A. Account B (Merchant) stays empty throughout.
-
Request testnet BTC to Account A from the Mezo Faucet or the
#testnetchannel in Mezo Discord. Wait for the drip to land in A’s wallet. -
Borrow MUSD at mezo.org/feature/borrow with Account A connected on Mezo Testnet. Full walkthrough with screenshots: Borrow and Mint MUSD.
-
Confirm Account A’s MUSD balance. The testnet MUSD token contract is
0x1189…Ac503. You have two ways to check:- Explorer: open
https://explorer.test.mezo.org/address/<account-A-address>(paste Account A’s0x…into the URL). You should see MUSD listed with ≥ 1,800 balance. - Wallet: add
0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503as a custom token. Account A will then show an MUSD line item.
- Explorer: open
-
Confirm Account B shows zero MUSD — it shouldn’t have any, and won’t until Step 7. Check at
https://explorer.test.mezo.org/address/<account-B-address>.
The MUSD token page itself looks like this — useful if you want to cross-check the canonical contract address, decimals (18), or scan recent transfers:
Step 3: Smoke test your wallet config
Section titled “Step 3: Smoke test your wallet config”Before installing any packages or writing any code, confirm that your wallet, network, and MUSD balance are all ready by paying a live x402 seller. This isolates “did I set up my wallet correctly?” from “did I wire my own server correctly?” Any trouble you hit here is a wallet or MUSD issue, not a code issue.
-
Open
https://demo.vativ.io/jokein the same browser that holds your Mezo Testnet wallet. Switch to Account A (Buyer) before connecting — A has the MUSD. -
The paywall loads. You should see:
- A Payment Required heading
please pay $0.001 Mezo USD- A wallet-select dropdown and Connect wallet button
-
Select your wallet and connect it from Account A. Confirm you are on Mezo Testnet (chain ID
31611) when the wallet prompts. -
Click Pay now, then sign the payment authorization in your wallet when prompted.
-
The paywall retries the request with your payment. Once the facilitator settles on chain, the joke renders in the browser.
-
Verify the payment on chain. Open
https://explorer.test.mezo.org/address/<account-A-address>— you should see a fresh MUSD transfer out of Account A for0.001 MUSDto thedemo.vativ.ioseller’s receiving address (your account B’s balance will not change).
If the joke appears and the explorer shows the transfer, your wallet setup is proven. Proceed to Step 4.
If you don’t reach the joke, stop here and fix the wallet side before writing any server code — see Troubleshooting for common wallet/MUSD symptoms. A broken wallet at this step will look identical to a broken server at Step 7; sort it out now.
Step 4: Install x402 packages
Section titled “Step 4: Install x402 packages”Create a fresh project for your own merchant server:
mkdir mezo-x402-server && cd mezo-x402-serverpnpm initpnpm add @x402/paywall @x402/evm @x402/core @x402/express expresspnpm add -D typescript tsx @types/express @types/nodeStep 5: Build your own seller
Section titled “Step 5: Build your own seller”The seller is an Express server that uses @x402/express middleware to
gate a route behind an MUSD payment. The middleware wires together three
pieces: an x402ResourceServer that negotiates with a facilitator, an
ExactEvmScheme registered for Mezo Testnet, and a route config that
says what the endpoint costs.
Use Account B (Merchant) as the receiving address. Switch to
Account B in your wallet and copy its 0x… address — that goes into
EVM_ADDRESS below. Account B never needs a balance; it only receives.
(Technically you could reuse Account A, but then a successful payment is invisible on chain: MUSD leaves A and lands in A, so the token balance doesn’t change. Two accounts give you a clean before/after signal in the explorer.)
Still inside mezo-x402-server/ (from Step 4), create server.ts:
import express, { type Request, type Response } from 'express';import { paymentMiddleware } from '@x402/express';import { x402ResourceServer, HTTPFacilitatorClient } from '@x402/core/server';import { ExactEvmScheme } from '@x402/evm/exact/server';import { createPaywall, evmPaywall } from '@x402/paywall';
const EVM_ADDRESS = process.env.EVM_ADDRESS; // Your Mezo Testnet receiving address (0x + 40 hex chars)const FACILITATOR_URL = 'https://facilitator.vativ.io/'; // Mezo-capable facilitator provided by community
if (!EVM_ADDRESS) throw new Error('Set EVM_ADDRESS to your Mezo Testnet receiving address.');if (!/^0x[0-9a-fA-F]{40}$/.test(EVM_ADDRESS)) { throw new Error(`EVM_ADDRESS must be a 0x-prefixed 40-hex-character address, got: ${EVM_ADDRESS}`);}
const app = express();
const facilitatorClient = new HTTPFacilitatorClient({ url: FACILITATOR_URL });
// Bundled paywall UI: knows how to render MUSD at 18 decimals and connect EVM// wallets. Without this, the middleware falls back to a stub that defaults to// USDC formatting (6 decimals) and shows an "Install @x402/paywall" placeholder.const paywall = createPaywall() .withNetwork(evmPaywall) .build();
app.use( paymentMiddleware( { 'GET /paid': { accepts: [ { scheme: 'exact', price: '$0.001', // charged in MUSD; resolves via DEFAULT_STABLECOINS network: 'eip155:31611', // Mezo Testnet, CAIP-2 string (not 'mezo-testnet') payTo: EVM_ADDRESS, }, ], description: 'A paid endpoint on Mezo', mimeType: 'application/json', }, }, new x402ResourceServer(facilitatorClient).register( 'eip155:*', // EVM-family scheme handler; route config above pins the actual chain new ExactEvmScheme(), ), undefined, // paywallConfig (defaults are fine) paywall, // ← the actual bundled UI provider ),);
app.get('/paid', (req: Request, res: Response) => { res.json({ message: 'Thank you for your payment.', servedAt: new Date().toISOString(), });});
app.listen(3000, () => { console.log('seller listening on http://localhost:3000/paid');});Step 6: Run your seller and pay it yourself
Section titled “Step 6: Run your seller and pay it yourself”Start the server with Account B’s address as the receiving address.
Substitute the 40-hex-character address you copied from Account B;
the literal placeholder below is not a valid hex address, so the
format guard in server.ts will reject it and make the typo obvious
instead of letting the server start with a nonsense payTo:
EVM_ADDRESS=0xYOUR_ACCOUNT_B_ADDRESS_HERE \ pnpm exec tsx server.tsYou should see seller listening on http://localhost:3000/paid. The
middleware will now intercept unpaid requests to /paid and respond
with HTTP 402 Payment Required. The response body depends on
the client’s Accept header: a browser (Accept: text/html) receives
a full HTML paywall page that renders the wallet-connect UI; other
clients (plain curl, fetch with no Accept, httpie, etc.)
receive a compact JSON body with the payment requirements in a
PAYMENT-REQUIRED response header. Either way, no client-side code
is required.
-
Sanity check that the server responds
402:Terminal window curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/paid# Expect: 402(The paywall UI only renders in a browser — that’s the next step.
curl -ion the same URL shows the402headers plus a compact JSON body with the payment requirements; the full HTML page is only served to browserAccept: text/htmlrequests.) -
Open
http://localhost:3000/paidin the same browser you used for the live-demo smoke test in Step 3. Switch to Account A (Buyer) before connecting — that’s the account with MUSD. -
The paywall loads — same UI as Step 3, but now served by your own code. The description text differs (“A paid endpoint on Mezo” comes from your
server.tsroute config rather than thedemo.vativ.iodemo’s copy): -
Connect Account A, confirm Mezo Testnet (chain ID
31611), click Pay now, and sign the authorization in your wallet. -
The paywall retries the request. Once the facilitator settles on chain, the JSON response body renders:
{"message": "Thank you for your payment.","servedAt": "2026-..."} -
Verify the A → B transfer on chain. Open both explorer tabs:
- Account A (debit):
https://explorer.test.mezo.org/address/<account-A-address> - Account B (credit):
https://explorer.test.mezo.org/address/<account-B-address>
You should see a single MUSD transfer of
0.001 MUSDleaving A and arriving at B at the timestamp of your payment. Click the transaction hash on either side to see the settlement details, including the facilitator address that paid gas. - Account A (debit):
That’s the full loop: you’ve just served a paywalled HTTP resource on Mezo Testnet, paid for it from Account A, and watched the MUSD land in Account B on chain.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
does not provide an export named 'DEFAULT_STABLECOINS' at runtime | A transitive dependency is pinning @x402/paywall/@x402/evm to 2.10.x, which lacks Mezo support | Force both to |
Paywall shows $10000000000.00 instead of $0.001 | Same as above — 2.10.x formats MUSD as 6-decimal USDC | Same fix — force |
EADDRINUSE: address already in use :::3000 | Another process is bound to port 3000 | Run lsof -i :3000 to find the holder, or change app.listen(3000, …) to a free port |
tsx: command not found | tsx dev dep didn’t install, or command is being run outside the project directory | Rerun pnpm add -D tsx inside the project; invoke as pnpm exec tsx server.ts |
SyntaxError on import / TS syntax errors from Node | Using Node.js < 20 | Upgrade to Node.js 20+ (node --version to check) |
| Wallet prompts to switch network / payment never confirms | Wallet is on the wrong Mezo network | Confirm the wallet is on the chain ID the seller expects (31611 for Testnet, 31612 for Mainnet) |
402 Payment Required returned but paywall UI never renders | network string is not a CAIP-2 identifier | Use 'eip155:31611' or 'eip155:31612' exactly; 'mezo-testnet' and bare 31611 are both rejected |
| Wallet refuses to add “Mezo Testnet” — or adds it but balances / transactions look wrong | Chain ID entered in hex (e.g. 0x7A5B) when the wallet field expects decimal, or Mainnet chain ID (31612) used when you wanted Testnet | Enter the chain ID as decimal 31611 for Testnet (31612 for Mainnet). The RPC URL must also point at testnet (https://rpc.test.mezo.org) — a testnet chain ID against a mainnet RPC silently produces a network that looks real but holds nothing |
Server errors immediately with EVM_ADDRESS must be a 0x-prefixed 40-hex-character address | Env var was not set, was missing the 0x prefix, contained non-hex characters, or wasn’t the right length | Copy the receiving address directly from your wallet (MetaMask’s address field always has the correct form); set it as EVM_ADDRESS=0x…. Do not wrap the value in quotes on the command line |
Facilitator returns unsupported network | FACILITATOR_URL in server.ts points at x402.org/facilitator (no Mezo support) | Set the constant to a Mezo-capable facilitator (https://facilitator.vativ.io/ for Testnet) |
| Paywall loads but Pay now never settles, no error | Wallet has no MUSD, or MUSD is on the wrong network | Revisit Step 2; confirm the testnet MUSD token contract shows a balance |
| Step 3 demo (demo.vativ.io) never settles | Same wallet/MUSD/network issue — isolate here before proceeding to Step 4 | Do not skip the Step 3 smoke test; fix the wallet side before writing server code |
See also
Section titled “See also”- MUSD Payments with x402. Conceptual overview.
- vativ/mezo-hack/apps/humor.
The Mezo-specific hackathon reference: server +
@x402/fetchclient wired to the mezo.6 preview tarballs, paywalledGET /jokeat0.001 MUSD, points atfacilitator.vativ.io, README walks through wallet funding + running the demo end-to-end. Clone and adapt. - Borrow and Mint MUSD ↗. User-side flow for acquiring MUSD (opens in new tab).
- Mezo Faucet. Testnet BTC.
