Ir al contenido

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.

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 paywall UI, connects a wallet, pays $0.001 MUSD on Mezo Testnet, and receives the protected API response.
  • 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.

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.

With either account selected, open your wallet’s networks settings and add a custom network with these parameters:

FieldValue
Network nameMezo Testnet
Chain ID31611
RPC URLhttps://rpc.test.mezo.org
Block explorerhttps://explorer.test.mezo.org
Currency symbolBTC

For mainnet parameters and alternative RPC providers, see Set Up Developer Environment.

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.

  1. Request testnet BTC to Account A from the Mezo Faucet or the #testnet channel in Mezo Discord. Wait for the drip to land in A’s wallet.

  2. Borrow MUSD at mezo.org/feature/borrow with Account A connected on Mezo Testnet. Full walkthrough with screenshots: Borrow and Mint MUSD.

  3. 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’s 0x… into the URL). You should see MUSD listed with ≥ 1,800 balance.
    • Wallet: add 0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503 as a custom token. Account A will then show an MUSD line item.
  4. 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:

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.

  1. Open https://demo.vativ.io/joke in the same browser that holds your Mezo Testnet wallet. Switch to Account A (Buyer) before connecting — A has the MUSD.

  2. The paywall loads. You should see:

    • A Payment Required heading
    • please pay $0.001 Mezo USD
    • A wallet-select dropdown and Connect wallet button
  3. Select your wallet and connect it from Account A. Confirm you are on Mezo Testnet (chain ID 31611) when the wallet prompts.

  4. Click Pay now, then sign the payment authorization in your wallet when prompted.

  5. The paywall retries the request with your payment. Once the facilitator settles on chain, the joke renders in the browser.

  6. 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 for 0.001 MUSD to the demo.vativ.io seller’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.

Create a fresh project for your own merchant server:

Terminal window
mkdir mezo-x402-server && cd mezo-x402-server
pnpm init
pnpm add @x402/paywall @x402/evm @x402/core @x402/express express
pnpm add -D typescript tsx @types/express @types/node

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:

Terminal window
EVM_ADDRESS=0xYOUR_ACCOUNT_B_ADDRESS_HERE \
pnpm exec tsx server.ts

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

  1. 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 -i on the same URL shows the 402 headers plus a compact JSON body with the payment requirements; the full HTML page is only served to browser Accept: text/html requests.)

  2. Open http://localhost:3000/paid in 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.

  3. 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.ts route config rather than the demo.vativ.io demo’s copy):

  4. Connect Account A, confirm Mezo Testnet (chain ID 31611), click Pay now, and sign the authorization in your wallet.

  5. The paywall retries the request. Once the facilitator settles on chain, the JSON response body renders:

    {
    "message": "Thank you for your payment.",
    "servedAt": "2026-..."
    }
  6. 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 MUSD leaving 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.

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.

SymptomCauseFix
does not provide an export named 'DEFAULT_STABLECOINS' at runtimeA transitive dependency is pinning @x402/paywall/@x402/evm to 2.10.x, which lacks Mezo support
Force both to ^2.11.0 via your package manager’s overrides
pnpm — add to package.json (top level, alongside dependencies), then pnpm install:
<br/>"pnpm": { "overrides": { "@x402/paywall": "^2.11.0", "@x402/evm": "^2.11.0" } }<br/>
npm — top-level "overrides": { ... }, then npm install.
yarn"resolutions": { ... }, then yarn install.
Override both packages together — @x402/paywall imports DEFAULT_STABLECOINS from @x402/evm.
Paywall shows $10000000000.00 instead of $0.001Same as above — 2.10.x formats MUSD as 6-decimal USDC
Same fix — force ^2.11.0 via overrides
See the row above for the full snippet (pnpm / npm / yarn variants).
EADDRINUSE: address already in use :::3000Another process is bound to port 3000Run lsof -i :3000 to find the holder, or change app.listen(3000, …) to a free port
tsx: command not foundtsx dev dep didn’t install, or command is being run outside the project directoryRerun pnpm add -D tsx inside the project; invoke as pnpm exec tsx server.ts
SyntaxError on import / TS syntax errors from NodeUsing Node.js < 20Upgrade to Node.js 20+ (node --version to check)
Wallet prompts to switch network / payment never confirmsWallet is on the wrong Mezo networkConfirm the wallet is on the chain ID the seller expects (31611 for Testnet, 31612 for Mainnet)
402 Payment Required returned but paywall UI never rendersnetwork string is not a CAIP-2 identifierUse '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 wrongChain ID entered in hex (e.g. 0x7A5B) when the wallet field expects decimal, or Mainnet chain ID (31612) used when you wanted TestnetEnter 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 addressEnv var was not set, was missing the 0x prefix, contained non-hex characters, or wasn’t the right lengthCopy 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 networkFACILITATOR_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 errorWallet has no MUSD, or MUSD is on the wrong networkRevisit Step 2; confirm the testnet MUSD token contract shows a balance
Step 3 demo (demo.vativ.io) never settlesSame wallet/MUSD/network issue — isolate here before proceeding to Step 4Do not skip the Step 3 smoke test; fix the wallet side before writing server code
  • MUSD Payments with x402. Conceptual overview.
  • vativ/mezo-hack/apps/humor. The Mezo-specific hackathon reference: server + @x402/fetch client wired to the mezo.6 preview tarballs, paywalled GET /joke at 0.001 MUSD, points at facilitator.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.
The x402 paywall UI at localhost:3000/paid before any wallet is connected: a 'Payment Required' heading, the text 'A paid endpoint on Mezo. To access this content, please pay $0.001 Mezo USD.', a 'Need Mezo USD on Mezo Testnet? Get some here.' link, and a wallet-select dropdown next to a blue Connect wallet button.