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

# Solana payments

> Step-by-step Meridian integration guide for x402 USDC payments on Solana (SVM)

Use this guide to integrate Meridian x402 payments on **Solana**, where the
buyer pays USDC (an SPL token) and settlement runs through the Meridian
`meridian_x402` program. It is written for sellers, buyers, and AI coding agents
implementing either side of the payment flow.

This path applies to the `solana` (mainnet-beta) and `solana-devnet` networks.
It is **same-chain only** — there is no Across cross-chain routing on Solana in
this version. Do not use the EIP-3009 or Permit2 flows here: Solana has no
EIP-712 signing, no Permit2, and no token approvals. The buyer signs the whole
settlement transaction instead, and that signature *is* the authorization.

The integration needs only `@solana/web3.js` and `@solana/spl-token` — **no
Anchor, no IDL, and no on-chain config reads**. Everything the buyer needs to
build the transaction comes from a single API call:
`GET /v1/solana/facilitator`.

<Note>
  The Solana program is live on both `solana` (mainnet) and `solana-devnet`.
  Do not hardcode the program ID. Read it (and everything else)
  from `GET /v1/solana/facilitator` at runtime so your integration keeps working
  across any redeploys. See [Solana Program](/payments/solana-program).
</Note>

## How the flow works

```text theme={"system"}
seller creates payment requirements (asset = USDC mint, payTo = recipient wallet)
buyer fetches GET /v1/solana/facilitator (fee payer, program id, mint, treasury, fees)
buyer builds a transfer_with_authorization transaction, fee payer = facilitator
buyer signs it once with their wallet (as USDC token authority)
buyer sends the base64 transaction to the seller in paymentPayload
seller settles through Meridian POST /v1/settle
Meridian validates, co-signs as fee payer, submits, and confirms on-chain
```

Meridian-specific rules:

* `paymentRequirements.asset` is the **USDC mint address**, not a token account
* `paymentRequirements.payTo` is the seller's **recipient wallet** (the owner,
  not a token account); it must match the Solana recipient on your Meridian
  organization
* `paymentRequirements.network` is `solana` or `solana-devnet`
* `paymentRequirements.extra.feePayer` is the facilitator pubkey from
  `GET /v1/solana/facilitator`; it pays the transaction fee and must equal the
  on-chain `config.backendSigner`
* `paymentRequirements.extra.name` is `"USDC"` and `extra.decimals` is `6`
* The buyer signs the full transaction; there is **no** typed-data signature,
  no token approval, and no Permit2
* All USDC associated token accounts (payer, recipient, platform, treasury) must
  **already exist** — the settlement transaction never creates accounts
* The buyer needs **no SOL**; the facilitator pays the transaction fee
* The seller keeps the Meridian API key on the backend

## Prerequisites

Seller:

* A Meridian organization with a **Solana recipient wallet** configured
* An organization API key from `https://mrdn.finance/dev/api-keys`
* A backend endpoint that can receive `paymentPayload` from the buyer

Buyer:

* A Solana wallet (Phantom, Solflare, Backpack, …)
* A USDC balance on the selected network — on `solana-devnet`, get test USDC
  from Circle's faucet at `https://faucet.circle.com` (select **Solana Devnet**)
* An existing USDC associated token account (the recipient's must exist too)

Install the client dependencies:

```bash theme={"system"}
npm install @solana/web3.js @solana/spl-token
```

## Network constants

```ts theme={"system"}
export const MERIDIAN_API_BASE = "https://api.mrdn.finance/v1";

// The facilitator base URL that serves GET /v1/solana/facilitator and POST /v1/settle.
export const FACILITATOR_URL = "https://api.mrdn.finance";

export const USDC_DECIMALS = 6;

export const SOLANA_NETWORKS = {
  solana: {
    cluster: "mainnet-beta",
    rpcUrl: "https://api.mainnet-beta.solana.com",
    usdcMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  },
  "solana-devnet": {
    cluster: "devnet",
    rpcUrl: "https://api.devnet.solana.com",
    usdcMint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
  },
} as const;
```

<Warning>
  The public RPC endpoints above are heavily rate-limited. Use a dedicated
  devnet/mainnet RPC (Helius, QuickNode, Triton, …) for anything beyond a quick
  test. The buyer only needs the RPC for a recent blockhash.
</Warning>

## The facilitator info endpoint

One GET returns everything needed to build the settlement transaction — the
fee-payer pubkey, program ID, config PDA, USDC mint, treasury token account,
protocol fee, and pause state (read on-chain by the facilitator and cached):

```ts theme={"system"}
export interface SolanaFacilitatorInfo {
  network: string;
  facilitator: string; // fee payer pubkey (== on-chain config.backendSigner)
  programId: string;
  configPda: string;
  usdcMint?: string; // enriched fields: present when the on-chain
  treasury?: string; //   config is readable (normal case)
  treasuryToken?: string; // treasury's USDC associated token account
  treasuryFeeBps?: number;
  paused?: boolean;
}

export async function getFacilitatorInfo(
  network: keyof typeof SOLANA_NETWORKS,
): Promise<SolanaFacilitatorInfo> {
  const res = await fetch(
    `${FACILITATOR_URL}/v1/solana/facilitator?network=${network}`,
    { cache: "no-store" },
  );
  if (!res.ok) {
    throw new Error(`facilitator info failed (${res.status})`);
  }
  return (await res.json()) as SolanaFacilitatorInfo;
}
```

The enriched fields (`usdcMint`, `treasury`, `treasuryToken`, `treasuryFeeBps`,
`paused`) mirror the program's on-chain `Config` account, so clients no longer
read it themselves. If they are missing, the facilitator could not reach the
chain — fail closed and retry rather than guessing values.

## Seller setup

### 1. Create a Meridian API key

Seller-side setup starts in Meridian:

1. Connect the seller wallet at `https://mrdn.finance`
2. Configure your organization's **Solana recipient wallet**
3. Open `https://mrdn.finance/dev/api-keys` and create an organization-scoped 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",
};
```

### 2. Build `paymentRequirements`

Use the USDC mint as `asset` and your recipient wallet as `payTo`. Fetch the
fee-payer pubkey from the facilitator and put it in `extra.feePayer`.

```ts theme={"system"}
const network = "solana-devnet";
const config = SOLANA_NETWORKS[network];
const amountInUsdcBaseUnits = "10000"; // 0.01 USDC (6 decimals)

const { facilitator } = await getFacilitatorInfo(network);

const paymentRequirements = {
  scheme: "exact",
  network,
  asset: config.usdcMint,
  payTo: process.env.SOLANA_RECIPIENT_WALLET, // owner wallet, not a token account
  maxAmountRequired: amountInUsdcBaseUnits,
  resource: "https://seller.example/api/tool",
  description: "Paid access to agent action",
  mimeType: "application/json",
  maxTimeoutSeconds: 600,
  extra: {
    name: "USDC",
    decimals: USDC_DECIMALS,
    feePayer: facilitator,
    creditedRecipient: process.env.SOLANA_RECIPIENT_WALLET,
  },
};
```

If you expose a standard HTTP 402 challenge, return:

```ts theme={"system"}
return Response.json(
  { x402Version: 1, accepts: [paymentRequirements] },
  { status: 402, headers: { "Cache-Control": "no-store, max-age=0" } },
);
```

### 3. Receive `paymentPayload`

The buyer sends a base64-encoded, already-signed Solana transaction under
`payload.transaction`. Build or retrieve the matching `paymentRequirements`
server-side; do not trust buyer-supplied requirements for pricing, recipient,
network, or asset.

```json theme={"system"}
{
  "x402Version": 1,
  "scheme": "exact",
  "network": "solana-devnet",
  "payload": {
    "transaction": "<base64 of the payer-signed settlement transaction>"
  }
}
```

Clients usually deliver this base64-encoded in an `X-PAYMENT` header; decode it
before settling:

```ts theme={"system"}
const header = request.headers.get("X-PAYMENT");
const paymentPayload = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
```

### 4. Settle with Meridian

Call `POST /v1/settle` from the seller backend. Meridian validates the signed
transaction, co-signs as fee payer, submits it, and confirms on-chain. 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.error ?? result.errorReason ?? "Meridian settlement failed");
}

// result.transaction is the confirmed Solana transaction signature.
const signature = result.transaction;
```

## Buyer setup

### 1. Select a payment requirement and fetch the facilitator info

```ts theme={"system"}
const requirements = accepts[0];
const network = requirements.network as keyof typeof SOLANA_NETWORKS;

const info = await getFacilitatorInfo(network);

// Sanity-check the 402 against the facilitator's authoritative config.
if (!info.usdcMint || !info.treasuryToken) {
  throw new Error("facilitator did not return on-chain config");
}
if (info.paused) throw new Error("Solana x402 program is paused");
if (requirements.extra.feePayer !== info.facilitator) {
  throw new Error("402 fee payer does not match the facilitator");
}
if (requirements.asset !== info.usdcMint) {
  throw new Error("402 asset does not match the USDC mint");
}
```

Because the info is fetched directly from the facilitator over HTTPS, it is the
trust anchor for the payment — a tampered 402 from a malicious seller cannot
redirect fees or swap the mint. (The paranoid can additionally verify the
returned values against the on-chain `Config` PDA at `info.configPda`, but this
is not required.)

### 2. Build the settlement transaction

Build one `transfer_with_authorization` instruction with plain
`@solana/web3.js` — no Anchor, no IDL. This helper is self-contained;
copy-paste it as-is:

```ts theme={"system"}
import {
  getAssociatedTokenAddressSync,
  TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { PublicKey, Transaction, TransactionInstruction } from "@solana/web3.js";

// transfer_with_authorization: anchor discriminator, stable across deployments.
const SETTLE_DISCRIMINATOR = Uint8Array.from([241, 208, 6, 43, 81, 61, 213, 10]);

export function buildSettleTransaction(params: {
  info: SolanaFacilitatorInfo;
  from: PublicKey; // buyer wallet
  recipient: PublicKey; // paymentRequirements.payTo (owner wallet)
  value: bigint; // USDC base units
  validAfter: bigint; // unix seconds, 0 = immediately valid
  validBefore: bigint; // unix seconds, e.g. now + maxTimeoutSeconds
  nonce: Uint8Array; // 32 random bytes
  recentBlockhash: string;
  platform?: PublicKey; // only in platform mode
  platformFeeBps?: number;
}): Transaction {
  const { info, from, recipient, recentBlockhash, platform } = params;
  if (!info.usdcMint || !info.treasuryToken) {
    throw new Error("facilitator did not return on-chain config");
  }
  if (info.paused) throw new Error("Solana settlement program is paused");
  if (params.nonce.length !== 32) throw new Error("nonce must be 32 bytes");

  const payer = new PublicKey(info.facilitator);
  const programId = new PublicKey(info.programId);
  const usdcMint = new PublicKey(info.usdcMint);

  // Instruction data: 66 bytes.
  const data = Buffer.alloc(66);
  data.set(SETTLE_DISCRIMINATOR, 0);
  data.writeBigUInt64LE(params.value, 8);
  data.writeBigInt64LE(params.validAfter, 16);
  data.writeBigInt64LE(params.validBefore, 24);
  data.set(params.nonce, 32);
  data.writeUInt16LE(params.platformFeeBps ?? 0, 64);

  // Recipient / platform / treasury owners may be off-curve (PDAs); the payer
  // signs, so their wallet must be a regular on-curve key.
  const instruction = new TransactionInstruction({
    programId,
    keys: [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: from, isSigner: true, isWritable: false },
      { pubkey: new PublicKey(info.configPda), isSigner: false, isWritable: false },
      { pubkey: usdcMint, isSigner: false, isWritable: false },
      {
        pubkey: getAssociatedTokenAddressSync(usdcMint, from),
        isSigner: false,
        isWritable: true,
      },
      {
        pubkey: getAssociatedTokenAddressSync(usdcMint, recipient, true),
        isSigner: false,
        isWritable: true,
      },
      // Optional platform slot: Anchor expects the PROGRAM ID here as a
      // placeholder when there is no platform fee — always pass 9 accounts.
      platform
        ? {
            pubkey: getAssociatedTokenAddressSync(usdcMint, platform, true),
            isSigner: false,
            isWritable: true,
          }
        : { pubkey: programId, isSigner: false, isWritable: false },
      {
        pubkey: new PublicKey(info.treasuryToken),
        isSigner: false,
        isWritable: true,
      },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
    ],
    data,
  });

  const tx = new Transaction({ feePayer: payer, recentBlockhash });
  tx.add(instruction);
  return tx;
}
```

Then use it:

```ts theme={"system"}
import { Connection } from "@solana/web3.js";

const connection = new Connection(SOLANA_NETWORKS[network].rpcUrl, "confirmed");
const { blockhash } = await connection.getLatestBlockhash("confirmed");

const tx = buildSettleTransaction({
  info,
  from: walletPublicKey, // the connected buyer wallet
  recipient: new PublicKey(requirements.payTo),
  value: BigInt(requirements.maxAmountRequired),
  validAfter: 0n,
  validBefore: BigInt(Math.floor(Date.now() / 1000) + requirements.maxTimeoutSeconds),
  nonce: crypto.getRandomValues(new Uint8Array(32)),
  recentBlockhash: blockhash,
});
```

<Note>
  If the monorepo package is available to you (Meridian's own apps), the same
  builder ships as `buildSettleTransaction` / `fetchSolanaFacilitatorInfo` from
  `@meridian/x402/shared/svm` — import it instead of copy-pasting.
</Note>

<Warning>
  Confirm the recipient's USDC associated token account exists before paying —
  the settlement transaction does not create it. Getting a
  `TokenAccountNotFoundError` at settle time means the recipient (or platform)
  ATA is missing.
</Warning>

### 3. Sign the transaction with the buyer wallet

The buyer signs once, as the USDC token authority. The facilitator adds the fee
payer's signature later, so serialize without requiring all signatures.

```ts theme={"system"}
const signed = await wallet.signTransaction(tx); // e.g. wallet-adapter

const transactionB64 = Buffer.from(
  signed.serialize({ requireAllSignatures: false, verifySignatures: false }),
).toString("base64");
```

### 4. Send `paymentPayload` to the seller

```ts theme={"system"}
const paymentPayload = {
  x402Version: 1,
  scheme: "exact",
  network: requirements.network,
  payload: { transaction: transactionB64 },
};

const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString(
  "base64",
);

await fetch("https://seller.example/api/tool", {
  method: "GET",
  headers: { "X-PAYMENT": paymentHeader },
});
```

## Instruction reference

For implementations in other languages, the layout (stable across deployments —
it is derived from the instruction name):

```text theme={"system"}
discriminator  8 bytes   [241, 208, 6, 43, 81, 61, 213, 10]
value          u64 LE     amount in USDC base units
valid_after    i64 LE     unix seconds (0 = immediately valid)
valid_before   i64 LE     unix seconds (now + maxTimeoutSeconds)
nonce          32 bytes   random
platform_fee_bps u16 LE   0 unless in platform mode
                          → total data length = 66 bytes
```

Account order — **always 9 accounts**, all non-signer/read-only unless noted:
`payer` (signer, writable), `from` (signer), `config`, `usdc_mint`,
`from_token` (writable), `recipient_token` (writable), `platform_token`
(writable — or the **program ID** as a read-only placeholder when there is no
platform fee), `treasury_token` (writable), `token_program`.

<Warning>
  Do not omit the `platform_token` slot when there is no platform fee. Anchor
  resolves optional accounts positionally: an 8-account list misaligns
  `treasury_token` and `token_program` and the facilitator rejects it. Pass the
  program ID in slot 6 as the "no platform" placeholder.
</Warning>

## Checklist for AI agents

* Read the seller's existing route structure before adding payment handling
* Keep settlement on the backend; never ship the Meridian API key to the client
* Use `network = "solana"` or `"solana-devnet"`
* Use `asset = USDC mint address`
* Use `payTo = the seller's recipient wallet` (owner, not a token account),
  matching the org's configured Solana recipient
* Fetch everything (`programId`, `facilitator`, `configPda`, `usdcMint`,
  `treasuryToken`, `paused`) from `GET /v1/solana/facilitator`; hardcode nothing
* Set `extra.feePayer` to the facilitator pubkey and `tx.feePayer` to it too
* Check `info.paused === false` and that the 402's `feePayer`/`asset` match the
  facilitator info before signing
* Ensure the recipient (and platform) USDC associated token account exists first
* Generate a fresh 32-byte nonce per payment
* Always pass 9 accounts (program ID placeholder in the platform slot)
* Have the buyer sign once and serialize with `requireAllSignatures: false`
* Send `payload.transaction` as base64; keep amounts as decimal strings

## Common mistakes

* Using EIP-3009 typed data, Permit2, or a token `approve` on Solana — none apply
* Setting `asset` to a token account instead of the USDC **mint**
* Setting `payTo` to a token account instead of the recipient **wallet owner**
* Hardcoding the program ID instead of reading it from the facilitator endpoint
* Omitting the `platform_token` slot (8 accounts) instead of passing the program
  ID placeholder — the account list must always be 9 long
* Setting `tx.feePayer` to the buyer wallet instead of the facilitator
* Paying to a recipient whose USDC associated token account does not exist yet
* Expecting the settlement transaction to create token accounts (it never does)
* Serializing with `requireAllSignatures: true` before the facilitator co-signs
* Reusing a nonce, or expecting cross-chain (Across) routing on Solana

## References

* [Solana Program](/payments/solana-program) — program architecture and accounts
* [Supported Networks](/api-reference/supported-networks)
* [Payment Types](/api-reference/payment-types/overview)
* [Settle x402 Payment](/api-reference/endpoint/settle-payment)
* [x402 SVM scheme](https://github.com/x402-foundation/x402)
