Skip to main content
This guide takes you from zero to accepting USDC payments through Meridian on any supported chain. The only dependency is viem — your server returns an x402 challenge, the buyer signs a payment authorization off-chain, and your server settles it through Meridian’s API. No transaction is submitted by the buyer and no gas is paid by either side.
1. Your server returns 402 + paymentRequirements
2. Buyer signs an EIP-3009 authorization (off-chain, gasless)
3. Buyer sends the signed payload to your server
4. Your server settles via Meridian POST /v1/settle
5. Meridian's facilitator executes the transfer on-chain
The code below is identical on every chain whose payment token supports EIP-3009 (Base, Optimism, Arbitrum, Polygon, Ink, and more) — only the network constants change. For chains without an EIP-3009 token (MegaETH, BSC, BOT chain, Tempo), the buyer signs a Permit2 witness instead; see the non-EIP-3009 guide.
1

Create an API key

Sign in with your wallet at mrdn.finance/dev/api-keys and create a key. Use a pk_test_ key for development and a pk_ key for production. Store it server-side:
MERIDIAN_API_KEY=pk_test_...
See API Keys for details.
2

Set your recipient wallet

In your organization settings, configure the wallet that receives settled funds. The recipient is tied to your API key — in the payment requirements, payTo always stays pointed at the Meridian facilitator contract, never at your own wallet.
3

Pick a network

Take the payment token and facilitator address for your chain from Supported Networks and keep them in one config object — everything below reads from it:
// Example: Base mainnet. Swap in any supported network's values.
const NETWORK = {
  id: "base",
  chainId: 8453,
  token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
  tokenName: "USD Coin", // EIP-712 domain name of the token
  tokenVersion: "2",     // EIP-712 domain version of the token
  facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
};
For development, use a testnet (pk_test_ key), e.g. Base Sepolia: chainId: 84532, token 0x036CbD53842c5426634e7929541eC2318f3dCF7e (tokenName: "USDC"), facilitator 0x8e633dBf31adCc7D41BE3e95B7c8DD3526B5235A. Test USDC is available from Circle’s faucet.A minimal Base Sepolia test should use:
  • MERIDIAN_API_KEY=pk_test_...
  • a buyer wallet connected to Base Sepolia (84532)
  • Base Sepolia USDC from Circle’s faucet
  • paymentRequirements.network: "base-sepolia"
  • paymentRequirements.payTo: "0x8e633dBf31adCc7D41BE3e95B7c8DD3526B5235A"
4

Return a 402 challenge from your server

When an unpaid request hits your paid endpoint, respond with HTTP 402 and the payment requirements:
const paymentRequirements = {
  scheme: "exact",
  network: NETWORK.id,
  asset: NETWORK.token,
  payTo: NETWORK.facilitator,
  maxAmountRequired: "10000", // $0.01 in USDC base units (6 decimals)
  resource: "https://your-api.example.com/api/premium",
  description: "Access to premium content",
  mimeType: "application/json",
  maxTimeoutSeconds: 300,
  extra: { name: NETWORK.tokenName, version: NETWORK.tokenVersion },
};

return Response.json(
  { x402Version: 1, accepts: [paymentRequirements] },
  { status: 402 },
);
To accept payments from more than one chain, return one requirement per source chain in accepts — see Payment Types.
5

Sign the payment in the buyer's browser

The buyer signs an EIP-3009 TransferWithAuthorization as typed data — no approval, no transaction:
import { bytesToHex } from "viem";

const now = BigInt(Math.floor(Date.now() / 1000));
const authorization = {
  from: account.address,
  to: paymentRequirements.payTo as `0x${string}`,
  value: BigInt(paymentRequirements.maxAmountRequired),
  validAfter: 0n,
  validBefore: now + BigInt(paymentRequirements.maxTimeoutSeconds),
  nonce: bytesToHex(crypto.getRandomValues(new Uint8Array(32))),
};

const signature = await walletClient.signTypedData({
  account,
  domain: {
    name: paymentRequirements.extra.name,
    version: paymentRequirements.extra.version,
    chainId: NETWORK.chainId,
    verifyingContract: paymentRequirements.asset as `0x${string}`,
  },
  types: {
    TransferWithAuthorization: [
      { name: "from", type: "address" },
      { name: "to", type: "address" },
      { name: "value", type: "uint256" },
      { name: "validAfter", type: "uint256" },
      { name: "validBefore", type: "uint256" },
      { name: "nonce", type: "bytes32" },
    ],
  },
  primaryType: "TransferWithAuthorization",
  message: authorization,
});
Then send the payload to your server (bigints as decimal strings):
const paymentPayload = {
  x402Version: 1,
  scheme: "exact",
  network: paymentRequirements.network,
  payload: {
    signature,
    authorization: {
      from: authorization.from,
      to: authorization.to,
      value: authorization.value.toString(),
      validAfter: authorization.validAfter.toString(),
      validBefore: authorization.validBefore.toString(),
      nonce: authorization.nonce,
    },
  },
};
6

Settle server-side

Your server settles the payment through Meridian, then serves the paid response. Never expose the API key to the browser, and do not trust buyer-supplied pricing, recipient, token, or network values. Rebuild or look up the matching paymentRequirements on the server before settlement:
if (paymentPayload.network !== paymentRequirements.network) {
  throw new Error("Payment network does not match the server requirement");
}

if (
  paymentPayload.payload.authorization.to.toLowerCase() !==
  paymentRequirements.payTo.toLowerCase()
) {
  throw new Error("Payment must authorize the Meridian facilitator");
}

if (
  paymentPayload.payload.authorization.value !==
  paymentRequirements.maxAmountRequired
) {
  throw new Error("Payment amount does not match the server price");
}
const response = await fetch("https://api.mrdn.finance/v1/settle", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MERIDIAN_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ paymentPayload, paymentRequirements }),
});

const result = await response.json();
if (!response.ok || !result.success) {
  throw new Error(result.errorReason ?? "Settlement failed");
}
7

Verify in Command Centre

Open Command Centre to see the payment, its settlement status, and your balance.

Next steps

Manual Integration

The full client-side flow: multi-chain challenges, requirement selection, and error handling.

Payment Types

EIP-3009, Permit2, and Circle Gateway — and when to use which.

Supported Networks

Chains, tokens, and contract addresses.

AI Skills

Self-contained integration instructions for AI coding agents.