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

# Manual Integration

> Integrate x402 payments into your frontend with viem — no SDK required

This guide shows the full x402 payment flow from a custom frontend using only
`viem`. Your resource server owns pricing, builds the `accepts` challenge, and
calls Meridian to settle the signed payment. The browser requests the resource,
signs the selected payment requirement, and resubmits the signed payload.

If you only need the shortest possible path on one network, start with the
[Quickstart](/quickstart). This guide adds multi-chain challenges, requirement
selection, and error handling on top of it.

## Prerequisites

* A frontend with wallet connection (MetaMask, WalletConnect, etc.)
* A protected resource endpoint that returns an x402 `402 Payment Required` challenge
* A server-side Meridian API key for the resource server

## Installation

```bash theme={"system"}
npm install viem
```

## Environment Setup

Keep the Meridian API key on your server. The browser only needs to know your
resource URL.

```bash theme={"system"}
# Browser-exposed client config
NEXT_PUBLIC_RESOURCE_URL=https://your-api.example.com/api/premium

# Server-side resource server config
MERIDIAN_API_KEY=pk_test_...
```

## Client Implementation

### 1. Wallet Connection

```typescript theme={"system"}
import { createWalletClient, custom, type WalletClient } from "viem";
// Import the chain(s) your app supports, e.g. base, optimism, ink, ...
import { base } from "viem/chains";

const connectWallet = async () => {
  const ethereum = (window as any).ethereum;
  if (!ethereum) {
    throw new Error("Please install MetaMask or another wallet extension");
  }

  const accounts = (await ethereum.request({
    method: "eth_requestAccounts",
  })) as `0x${string}`[];

  if (accounts.length === 0) {
    throw new Error("No wallet account selected");
  }

  const account = accounts[0];
  const walletClient = createWalletClient({
    account,
    transport: custom(ethereum),
    chain: base, // the chain the buyer pays from
  });

  return { account, walletClient };
};
```

### 2. Request the Protected Resource

Request the resource without payment first. A `402` response carries the
challenge with one payment requirement per accepted source chain.

```typescript theme={"system"}
interface PaymentRequirements {
  scheme: string;
  network: string;
  asset: string;
  payTo: string;
  maxAmountRequired: string;
  resource: string;
  description: string;
  mimeType: string;
  maxTimeoutSeconds: number;
  extra?: {
    name?: string;
    version?: string;
    creditedRecipient?: string;
    destinationChainId?: number;
  };
}

interface X402Challenge {
  x402Version: number;
  accepts: PaymentRequirements[];
  error?: string;
}

const RESOURCE_URL = process.env.NEXT_PUBLIC_RESOURCE_URL!;

const requestResource = async (
  walletClient: WalletClient,
  account: `0x${string}`,
) => {
  const response = await fetch(RESOURCE_URL);

  if (response.status === 402) {
    const challenge = (await response.json()) as X402Challenge;
    return payAndRetry(challenge, walletClient, account);
  }

  if (!response.ok) {
    throw new Error(`Resource request failed: ${response.status}`);
  }

  return response.json();
};
```

### 3. Select the Payment Requirement

Pick the `accepts` entry whose `network` matches the buyer's connected chain.
A requirement is source-chain specific — a Base entry cannot be paid from Ink,
because the EIP-712 signature domain includes the source chain id and source
token contract.

```typescript theme={"system"}
// Chain ids for the networks your app supports
const NETWORK_CHAIN_IDS: Record<string, number> = {
  "base-sepolia": 84532,
  base: 8453,
  ink: 57073,
  optimism: 10,
};

const selectRequirement = (
  accepts: PaymentRequirements[],
  connectedChainId: number,
): PaymentRequirements => {
  const match = accepts.find(
    (entry) => NETWORK_CHAIN_IDS[entry.network] === connectedChainId,
  );
  if (!match) {
    throw new Error("No payment route matches the connected wallet network");
  }
  return match;
};
```

### 4. Sign and Retry

Sign the EIP-3009 `TransferWithAuthorization` as typed data and resubmit the
request with the signed payload. The verifying contract is the token
(`requirement.asset`), not the facilitator, and `authorization.to` is the
facilitator (`requirement.payTo`).

```typescript theme={"system"}
import { bytesToHex } from "viem";

const payAndRetry = async (
  challenge: X402Challenge,
  walletClient: WalletClient,
  account: `0x${string}`,
) => {
  const chainId = walletClient.chain!.id;
  const requirement = selectRequirement(challenge.accepts, chainId);

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

  const signature = await walletClient.signTypedData({
    account,
    domain: {
      name: requirement.extra?.name ?? "USD Coin",
      version: requirement.extra?.version ?? "2",
      chainId,
      verifyingContract: requirement.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,
  });

  const paymentPayload = {
    x402Version: 1,
    scheme: "exact",
    network: requirement.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,
      },
    },
  };

  const response = await fetch(RESOURCE_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ paymentPayload }),
  });

  if (response.status === 402) {
    const body = await response.json();
    throw new Error(body?.error ?? "Payment was rejected");
  }

  if (!response.ok) {
    throw new Error(`Paid resource request failed: ${response.status}`);
  }

  return response.json();
};
```

<Note>
  On chains whose payment token does not support EIP-3009 (MegaETH, BSC, BOT
  chain, Tempo), the buyer signs a Permit2 witness instead of
  `TransferWithAuthorization`. That is a separate payload shape and signing
  flow, not a small variation of the code above. See the [non-EIP-3009
  guide](/ai-skills/non-eip-3009) for the Permit2 implementation.
</Note>

## Server Responsibilities

Your resource server must not trust buyer-supplied pricing, recipient, network,
or token data. Build or look up the matching `paymentRequirements` server-side,
validate that the signed payload is bound to that requirement, then settle the
buyer's `paymentPayload` through Meridian:

```typescript theme={"system"}
const paymentRequirements = lookupRequirementForAcceptedNetwork(
  paymentPayload.network,
);

if (!paymentRequirements) {
  throw new Error("Unsupported payment network");
}

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");
}
```

```typescript theme={"system"}
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 ?? "Meridian settlement failed");
}
```

For multi-chain sellers, include one `paymentRequirements` object per source
chain in `accepts`. Set `network`, `asset`, and `payTo` for the source chain,
then set `extra.destinationChainId` to the destination chain where Meridian
should settle after Across fills. Add `extra.creditedRecipient` when the payout
should go to a specific merchant or marketplace recipient — see
[Marketplace Fees](/command-centre/marketplace).

The full request shape, including cross-chain and Permit2 payloads, is
documented in [Settle x402 Payment](/api-reference/endpoint/settle-payment).
