Skip to main content
Use this guide to integrate Meridian x402 payments on EVM chains where the payment token does not expose native EIP-3009 transferWithAuthorization. It is written for sellers, buyers, and AI coding agents implementing either side of the payment flow. This current path uses Permit2 and applies to MegaETH, BSC, BOT Chain, BOT Chain testnet, and Tempo. Do not use the legacy BSC or MegaETH forwarders for new integrations.

How the flow works

seller creates payment requirements
buyer approves Permit2
buyer signs a Permit2 witness authorization
buyer sends paymentPayload to seller
seller settles through Meridian POST /v1/settle
Meridian facilitator pulls funds and distributes proceeds
Meridian-specific rules:
  • paymentRequirements.payTo is the Meridian facilitator, not the seller wallet
  • paymentRequirements.asset is the ERC-20 token the buyer pays with
  • The buyer approves Permit2, not the facilitator and not the exact proxy
  • The buyer signs with x402ExactPermit2Proxy as the Permit2 spender
  • witness.to is the Meridian facilitator
  • The seller keeps the Meridian API key on the backend

Prerequisites

Seller:
  • A Meridian organization
  • An organization API key from https://mrdn.finance/dev/api-keys
  • A backend endpoint that can receive paymentPayload from the buyer
Buyer:
  • A wallet on the selected chain
  • Balance of the selected ERC-20 token
  • Permit2 allowance for that token
Install the common client dependencies:
npm install viem @uniswap/permit2-sdk

Network constants

The token addresses below are Meridian’s default payment tokens for each network. If your application accepts another transferable ERC-20 on the same chain, use that token address in paymentRequirements.asset and use that token’s real name, version, and decimals.
export const MERIDIAN_API_BASE = "https://api.mrdn.finance/v1";
export const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
export const X402_EXACT_PERMIT2_PROXY =
  "0x402085c248EeA27D92E8b30b2C58ed07f9E20001";

export const NON_EIP_3009_NETWORKS = {
  megaeth: {
    chainId: 4326,
    token: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7",
    tokenName: "USDm",
    tokenVersion: "1",
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
  bsc: {
    chainId: 56,
    token: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
    tokenName: "USDC",
    tokenVersion: "1",
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
  "bot-chain": {
    chainId: 677,
    token: "0xaBabc7Ddc03e501d190C676BF3d92ef0e6e87a3C",
    tokenName: "USDT",
    tokenVersion: "1",
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
  "bot-chain-testnet": {
    chainId: 968,
    token: "0x75edC9335175Fc0552D51D48439F229c10420fe3",
    tokenName: "USDT",
    tokenVersion: "1",
    facilitator: "0x8e633dBf31adCc7D41BE3e95B7c8DD3526B5235A",
  },
  tempo: {
    chainId: 4217,
    token: "0x20C000000000000000000000b9537d11c60E8b50",
    tokenName: "USDC.e",
    tokenVersion: "1",
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
} as const;

Seller setup

1. Create a Meridian API key

Seller-side setup starts in Meridian:
  1. Connect the seller wallet at https://mrdn.finance
  2. Open https://mrdn.finance/dev/api-keys
  3. Create an organization-scoped API key
  4. Store the public pk_... key on the server
Use the key in the Authorization header when calling Meridian:
const meridianHeaders = {
  Authorization: `Bearer ${process.env.MERIDIAN_API_KEY}`,
  "Content-Type": "application/json",
};
The API key can also be created programmatically with a valid SIWE session cookie:
curl -X POST https://api.mrdn.finance/v1/api_keys \
  -H "Content-Type: application/json" \
  -H "Cookie: siwe-session=your_session" \
  -d '{
    "name": "My Application Key",
    "test_net": true
  }'

2. Choose the network and amount

Amounts must use the selected token’s base units. Do not assume every token has 6 decimals.
const network = "exampleNetwork";
const config = NON_EIP_3009_NETWORKS[network];
const amountInTokenBaseUnits = 1_000_000_000_000_000_000n;

3. Build paymentRequirements

Use the ERC-20 token address as asset. Keep payTo pointed at the Meridian facilitator.
const paymentRequirements = {
  scheme: "exact",
  network,
  asset: config.token,
  payTo: config.facilitator,
  maxAmountRequired: amountInTokenBaseUnits.toString(),
  resource: "https://seller.example/api/tool",
  description: "Paid access to agent action",
  mimeType: "application/json",
  maxTimeoutSeconds: 300,
  extra: {
    name: config.tokenName,
    version: config.tokenVersion,
  },
};
If you expose a standard HTTP 402 challenge, return:
return Response.json(
  {
    x402Version: 1,
    accepts: [paymentRequirements],
  },
  { status: 402 },
);

4. Receive paymentPayload

The buyer sends raw JSON, not a transaction they submit on-chain. Build or retrieve the matching paymentRequirements server-side; do not trust buyer-supplied requirements for pricing, recipient, network, or token selection.
{
  "x402Version": 1,
  "scheme": "exact",
  "network": "exampleNetwork",
  "payload": {
    "signature": "0x...",
    "owner": "0x...",
    "permit": {
      "permitted": {
        "token": "0xYourErc20TokenAddress",
        "amount": "123456789"
      },
      "nonce": "123",
      "deadline": "1741885200"
    },
    "witness": {
      "to": "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
      "validAfter": "0"
    }
  }
}

5. Settle with Meridian

Call POST /v1/settle from the seller backend. Do not expose the Meridian API key to an untrusted client.
const response = await fetch(`${MERIDIAN_API_BASE}/settle`, {
  method: "POST",
  headers: meridianHeaders,
  body: JSON.stringify({
    paymentPayload,
    paymentRequirements,
  }),
});

const result = await response.json();

if (!response.ok || !result.success) {
  throw new Error(result.errorReason ?? "Meridian settlement failed");
}

Buyer setup

1. Select a payment requirement

Use the seller-provided requirement for the network and token the buyer will pay with.
const paymentRequirements = accepts[0];
const network =
  paymentRequirements.network as keyof typeof NON_EIP_3009_NETWORKS;
const config = NON_EIP_3009_NETWORKS[network];

2. Approve Permit2

The buyer approves the canonical Permit2 contract as spender for the token in paymentRequirements.asset. The buyer does not approve the x402ExactPermit2Proxy or the facilitator to spend their tokens!
import { erc20Abi, maxUint256 } from "viem";

const token = paymentRequirements.asset as `0x${string}`;

await walletClient.writeContract({
  address: token,
  abi: erc20Abi,
  functionName: "approve",
  args: [PERMIT2, maxUint256],
});
In production, check allowance first and only prompt for approval when needed.

3. Sign the Permit2 authorization

Sign a Permit2 witness transfer. The EIP-712 verifying contract is Permit2, the Permit2 spender is x402ExactPermit2Proxy, and the witness destination is the Meridian facilitator.
import { randomBytes } from "node:crypto";
import { SignatureTransfer } from "@uniswap/permit2-sdk";

const nonce = BigInt(`0x${randomBytes(32).toString("hex")}`);

const permit = {
  permitted: {
    token,
    amount: BigInt(paymentRequirements.maxAmountRequired),
  },
  spender: X402_EXACT_PERMIT2_PROXY,
  nonce,
  deadline: BigInt(
    Math.floor(Date.now() / 1000) + paymentRequirements.maxTimeoutSeconds,
  ),
};

const witness = {
  to: paymentRequirements.payTo as `0x${string}`,
  validAfter: 0n,
};

const witnessType = {
  Witness: [
    { name: "to", type: "address" },
    { name: "validAfter", type: "uint256" },
  ],
};

const { domain, types, values } = SignatureTransfer.getPermitData(
  permit,
  PERMIT2,
  config.chainId,
  witness,
  witnessType,
);

const signature = await walletClient.signTypedData({
  account,
  domain,
  types,
  primaryType: "PermitWitnessTransferFrom",
  message: values,
});

4. Send paymentPayload to the seller

Serialize bigint values as decimal strings.
const paymentPayload = {
  x402Version: 1,
  scheme: "exact",
  network: paymentRequirements.network,
  payload: {
    signature,
    owner: account.address,
    permit: {
      permitted: {
        token: permit.permitted.token,
        amount: permit.permitted.amount.toString(),
      },
      nonce: permit.nonce.toString(),
      deadline: permit.deadline.toString(),
    },
    witness: {
      to: witness.to,
      validAfter: witness.validAfter.toString(),
    },
  },
};

await fetch("https://seller.example/api/tool", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ paymentPayload }),
});
If the token supports ERC-2612 and you want a first-payment flow without a standing Permit2 approval, attach a verified permit2612 object to paymentPayload.payload. Only use this shortcut for tokens you have confirmed implement permit().

Checklist for AI agents

  • Read the seller’s existing route structure before adding payment handling
  • Keep settlement on the backend
  • Use asset = token address
  • Use payTo = Meridian facilitator
  • Use permit.spender = x402ExactPermit2Proxy
  • Use domain.verifyingContract = Permit2
  • Use witness.to = Meridian facilitator
  • Convert every bigint in paymentPayload to a string before JSON encoding
  • Do not add legacy forwarder logic for new integrations

Common mistakes

  • Setting payTo to the seller wallet
  • Approving the exact proxy or facilitator instead of Permit2
  • Signing with Permit2 as spender instead of x402ExactPermit2Proxy
  • Setting witness.to to anything other than the Meridian facilitator
  • Reusing hardcoded decimals from another chain
  • Sending raw bigint values through JSON
  • Putting the Meridian API key in browser code

References