> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mrdn.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# EIP-3009 payments

> Step-by-step Meridian integration guide for native transferWithAuthorization chains

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.

For USDC routes supported by Across, this same EIP-3009 path can settle
cross-chain. The seller exposes one `paymentRequirements` entry per source
chain the buyer can pay from, and uses `extra.destinationChainId` when the
recipient should be paid on a different chain.

## How the flow works

```text theme={"system"}
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
Meridian distributes proceeds locally or via Across to the destination chain
```

Meridian-specific rules:

* `paymentRequirements.payTo` is the Meridian facilitator, not the seller wallet
* `paymentRequirements.asset` is the token that implements EIP-3009
* `paymentRequirements.network` is the source chain the buyer pays from
* `authorization.to` is the Meridian facilitator
* The EIP-712 verifying contract is `paymentRequirements.asset`
* The EIP-712 primary type is `TransferWithAuthorization`
* `paymentRequirements.extra.destinationChainId` enables an Across-backed
  destination chain when it differs from the source chain
* `paymentRequirements.extra.creditedRecipient` can carry the seller's final
  recipient address when it differs from the facilitator-bound `payTo`
* 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:

```bash theme={"system"}
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.

```ts theme={"system"}
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:

```ts theme={"system"}
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:

```bash theme={"system"}
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 source token's base units. USDC-style EIP-3009
tokens usually have 6 decimals, but still bind the amount to the configured
source token.

```ts theme={"system"}
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.

```ts theme={"system"}
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:

```ts theme={"system"}
return Response.json(
  {
    x402Version: 1,
    accepts: [paymentRequirements],
  },
  { status: 402 },
);
```

### 4. Expose multiple source-chain routes

If the seller wants to accept payments from several chains, return one
`paymentRequirements` object per accepted source chain in `accepts`. The buyer
or SDK selects the entry matching the buyer's connected wallet chain.

For a seller that wants to receive on Base but accept payment from Base, Ink,
and Optimism:

```ts theme={"system"}
const destinationNetwork = "base";
const destinationChainId = EIP_3009_NETWORKS[destinationNetwork].chainId;
const sellerBaseRecipient = "0x1111111111111111111111111111111111111111";

const sourceNetworks = ["base", "ink", "optimism"] as const;

const accepts = sourceNetworks.map((sourceNetwork) => {
  const source = EIP_3009_NETWORKS[sourceNetwork];

  return {
    scheme: "exact",
    network: sourceNetwork,
    asset: source.token,
    payTo: source.facilitator,
    maxAmountRequired: amountInTokenBaseUnits.toString(),
    resource: "https://seller.example/api/tool",
    description: "Paid access to agent action",
    mimeType: "application/json",
    maxTimeoutSeconds: 300,
    extra: {
      name: source.tokenName,
      version: source.tokenVersion,
      creditedRecipient: sellerBaseRecipient,
      destinationChainId,
    },
  };
});

return Response.json({ x402Version: 1, accepts }, { status: 402 });
```

In this model, an Ink-funded buyer pays the `network: "ink"` requirement. The
buyer signs on Ink against Ink USDC, Meridian pulls funds into the Ink
facilitator, and Across routes the output to Base before final settlement to
`creditedRecipient`.

Do not return only a Base requirement if you expect buyers to pay from Ink,
Optimism, or another chain. A `paymentRequirements` object is source-chain
specific because the EIP-712 signature domain includes the source chain and
source token contract.

Across currently uses exact-input routing in this flow. If the seller must
receive an exact destination-chain amount, quote the route and set each
source-chain `maxAmountRequired` high enough to cover Across fees.

### 5. 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.

```json theme={"system"}
{
  "x402Version": 1,
  "scheme": "exact",
  "network": "base",
  "payload": {
    "signature": "0x...",
    "authorization": {
      "from": "0x2222222222222222222222222222222222222222",
      "to": "0x8E7769D440b3460b92159Dd9C6D17302b036e2d6",
      "value": "1000000",
      "validAfter": "0",
      "validBefore": "1741885200",
      "nonce": "0x..."
    }
  }
}
```

### 6. Settle with Meridian

Call `POST /v1/settle` from the seller backend. Do not expose the Meridian API
key to an untrusted client.

```ts theme={"system"}
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. If the seller returned several `accepts` entries, choose the route
whose `network` matches the buyer's connected source chain.

```ts theme={"system"}
const buyerNetwork = "ink";
const paymentRequirements =
  accepts.find((entry) => entry.network === buyerNetwork) ?? accepts[0];
const network = paymentRequirements.network as keyof typeof EIP_3009_NETWORKS;
const config = EIP_3009_NETWORKS[network];
```

For cross-chain routes, do not change the signing domain to the destination
chain. The buyer always signs on the source chain selected by
`paymentRequirements.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.

```ts theme={"system"}
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.

```ts theme={"system"}
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`
* For multi-chain sellers, return one `accepts` entry per source chain
* For Across routes, keep `network`, `asset`, and `payTo` source-chain based
* For Across routes, set `extra.destinationChainId` to the settlement chain id
* Put the final seller recipient in organization settings or
  `extra.creditedRecipient`
* 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
* Returning only a Base requirement when buyers need to pay from Ink or another
  source chain
* Signing with the destination chain id for a cross-chain payment
* 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

* [Supported Networks](/api-reference/supported-networks)
* [Payment Types](/api-reference/payment-types/overview)
* [Settle x402 Payment](/api-reference/endpoint/settle-payment)
* [Smart Contracts](/payments/smart-contracts)
* [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009)
