Skip to main content
Use this guide to integrate Meridian x402 payments on EVM chains where the payment token exposes native EIP-3009 transferWithAuthorization. It is written for sellers, buyers, and AI coding agents implementing either side of the payment flow. This path applies to chains such as Base, Ink, HyperEVM, Optimism, Polygon, and Unichain when the selected payment token natively supports EIP-3009. Do not use Permit2 on these chains for the normal USDC x402 flow. Permit2 is only for chains whose payment token does not expose EIP-3009, such as MegaETH, BSC, and BOT Chain.

How the flow works

seller creates payment requirements
buyer signs an EIP-3009 transferWithAuthorization
buyer sends paymentPayload to seller
seller settles through Meridian POST /v1/settle
Meridian facilitator relays transferWithAuthorization and distributes proceeds
Meridian-specific rules:
  • paymentRequirements.payTo is the Meridian facilitator, not the seller wallet
  • paymentRequirements.asset is the token that implements EIP-3009
  • authorization.to is the Meridian facilitator
  • The EIP-712 verifying contract is paymentRequirements.asset
  • The EIP-712 primary type is TransferWithAuthorization
  • The buyer does not approve Permit2, the facilitator, or any proxy
  • The buyer does not sign a Permit2 witness payload
  • 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 EIP-3009 payment token
Install the common client dependency:
npm install viem
Do not install @uniswap/permit2-sdk for this flow.

Network constants

The token addresses below are Meridian’s default EIP-3009 payment tokens for common networks. If your application accepts another token on the same chain, only use this flow after confirming that token implements EIP-3009 transferWithAuthorization. Otherwise use the non-EIP-3009 Permit2 guide.
export const MERIDIAN_API_BASE = "https://api.mrdn.finance/v1";

export const EIP_3009_NETWORKS = {
  base: {
    chainId: 8453,
    token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    tokenName: "USD Coin",
    tokenVersion: "2",
    decimals: 6,
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
  ink: {
    chainId: 57073,
    token: "0x2D270e6886d130D724215A266106e6832161EAEd",
    tokenName: "USD Coin",
    tokenVersion: "2",
    decimals: 6,
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
  hyperevm: {
    chainId: 999,
    token: "0xb88339CB7199b77E23DB6E890353E22632Ba630f",
    tokenName: "USD Coin",
    tokenVersion: "2",
    decimals: 6,
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
  optimism: {
    chainId: 10,
    token: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
    tokenName: "USD Coin",
    tokenVersion: "2",
    decimals: 6,
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
  polygon: {
    chainId: 137,
    token: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
    tokenName: "USD Coin",
    tokenVersion: "2",
    decimals: 6,
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
  unichain: {
    chainId: 130,
    token: "0x078d782b760474a361dda0af3839290b0ef57ad6",
    tokenName: "USD Coin",
    tokenVersion: "2",
    decimals: 6,
    facilitator: "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
  },
  "base-sepolia": {
    chainId: 84532,
    token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    tokenName: "USDC",
    tokenVersion: "2",
    decimals: 6,
    facilitator: "0x8e633dBf31adCc7D41BE3e95B7c8DD3526B5235A",
  },
  "arc-testnet": {
    chainId: 5042002,
    token: "0x3600000000000000000000000000000000000000",
    tokenName: "USDC",
    tokenVersion: "1",
    decimals: 6,
    facilitator: "0x8e633dBf31adCc7D41BE3e95B7c8DD3526B5235A",
  },
} 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. USDC-style EIP-3009 tokens usually have 6 decimals, but still bind the amount to the configured token.
const network = "base";
const config = EIP_3009_NETWORKS[network];
const amountInTokenBaseUnits = 1_000_000n;

3. Build paymentRequirements

Use the EIP-3009 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": "base",
  "payload": {
    "signature": "0x...",
    "authorization": {
      "from": "0xBuyerWallet",
      "to": "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
      "value": "1000000",
      "validAfter": "0",
      "validBefore": "1741885200",
      "nonce": "0x..."
    }
  }
}

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 EIP_3009_NETWORKS;
const config = EIP_3009_NETWORKS[network];

2. Sign TransferWithAuthorization

The buyer signs typed data against the token contract. There is no ERC-20 approval, no Permit2 allowance, and no witness object.
import { bytesToHex } from "viem";

const transferWithAuthorizationTypes = {
  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" },
  ],
} as const;

const randomNonce32 = () =>
  bytesToHex(crypto.getRandomValues(new Uint8Array(32)));

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: randomNonce32(),
};

const signature = await walletClient.signTypedData({
  account,
  domain: {
    name: paymentRequirements.extra?.name ?? config.tokenName,
    version: paymentRequirements.extra?.version ?? config.tokenVersion,
    chainId: config.chainId,
    verifyingContract: paymentRequirements.asset as `0x${string}`,
  },
  types: transferWithAuthorizationTypes,
  primaryType: "TransferWithAuthorization",
  message: authorization,
});

3. Send paymentPayload to the seller

Serialize bigint values as decimal strings. Keep the nonce as the original 32-byte hex string.
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,
    },
  },
};

await fetch("https://seller.example/api/tool", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ paymentPayload }),
});

Checklist for AI agents

  • Read the seller’s existing route structure before adding payment handling
  • Keep settlement on the backend
  • Use this guide for Base, Ink, HyperEVM, and other EIP-3009 token chains
  • Use asset = EIP-3009 token address
  • Use payTo = Meridian facilitator
  • Use authorization.to = Meridian facilitator
  • Use domain.verifyingContract = paymentRequirements.asset
  • Use primaryType = TransferWithAuthorization
  • Generate a fresh 32-byte nonce per payment
  • Convert every bigint in paymentPayload to a string before JSON encoding
  • Do not add Permit2 approval, Permit2 SDK code, or Permit2 witness fields

Common mistakes

  • Using Permit2 on Base, Ink, HyperEVM, or another native EIP-3009 token chain
  • Setting payTo to the seller wallet
  • Setting authorization.to to the seller wallet
  • Signing with the facilitator as the EIP-712 verifying contract
  • Using the wrong token name or version in the EIP-712 domain
  • Reusing an EIP-3009 nonce
  • Sending raw bigint values through JSON
  • Putting the Meridian API key in browser code

References