Overview
meridian_x402 is the Solana (SVM) port of the Meridian facilitator. It is the
equivalent of X402ProxyFacilitator.transferWithAuthorization
on EVM: it pulls USDC from a payer, subtracts platform and treasury fees, and
settles the remainder to the recipient — atomically, in a single transaction.
This first version is intentionally minimal. It settles same-chain USDC
payments on Solana and nothing else: no cross-chain (Across) routing, no MRDN
cashback, and no batched settlement (those live only on the EVM facilitator
today).
Integrations should read the program ID, facilitator pubkey, and on-chain
config from GET /v1/solana/facilitator at runtime rather than hardcoding
them, so they keep working across any redeploys.
Deployment
| Network | Cluster | Program ID | Status |
|---|
solana | mainnet-beta | Ro6hz1smrm5zDh73849eDqKna9dE1EkPsWekAB5rBWm | Live |
solana-devnet | devnet | Ro6hz1smrm5zDh73849eDqKna9dE1EkPsWekAB5rBWm | Live |
Built with Anchor 0.32.1. The config account is a PDA at seeds ["config"],
derived from the program ID.
USDC mints the program settles against:
| Network | USDC Mint |
|---|
solana | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
solana-devnet | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU |
Why it looks different from the EVM facilitator
On EVM, the payer cannot join the settlement transaction without paying gas, so
x402 relies on EIP-3009: the payer signs an off-chain authorization, and the
facilitator submits it on-chain where the token contract verifies the signature.
Solana needs none of that machinery:
- A transaction can have multiple signers, and any of them can pay the fee.
The payer (
from) signs the settlement transaction as the authority of their
USDC token account. The backend signer co-signs as payer (fee payer) and
submits it. The payer’s transaction signature is the authorization — no
in-program signature verification required.
- Settlement is atomic within one transaction, so there is no escrow hop.
The EVM contract first pulls the full amount into itself and then distributes;
here the program splits funds directly from the payer’s token account via up
to three
transfer_checked CPIs (platform fee, treasury fee, recipient net).
From the client’s perspective the x402 flow is unchanged:
What is kept for parity with the EVM transferWithAuthorization:
valid_after / valid_before settlement window, checked against the on-chain clock
- a 32-byte
nonce for x402 payment correlation and idempotency
- identical fee economics (platform fee off gross, treasury fee off the remainder)
Architecture
Config account
A single global PDA (seeds = ["config"]) holds the program’s configuration:
| Field | Type | Purpose |
|---|
authority | Pubkey | Admin: fee updates, pause, treasury / signer rotation |
backend_signer | Pubkey | Backend signer allowed to submit settlements |
treasury | Pubkey | Wallet that owns the treasury-fee token account |
usdc_mint | Pubkey | The only mint the program settles |
treasury_fee_bps | u16 | Protocol fee in basis points (max 1000 = 10%) |
paused | bool | Circuit breaker for the settlement entrypoint |
bump | u8 | Config PDA bump |
Instructions
| Instruction | Access | Purpose |
|---|
initialize(backend_signer, treasury, treasury_fee_bps) | one-time | Create the Config PDA |
transfer_with_authorization(value, valid_after, valid_before, nonce, platform_fee_bps) | payer + backend signer sign | Settle a payment |
set_treasury_fee_bps(bps) | authority | Update the protocol fee |
set_treasury(pubkey) | authority | Rotate the treasury wallet |
set_backend_signer(pubkey) | authority | Rotate the backend signer |
set_paused(bool) | authority | Pause / unpause settlement |
transfer_with_authorization account list:
| Account | Signer | Notes |
|---|
payer | ✓ | Backend signer (facilitator): pays the tx fee |
from | ✓ | The paying user (USDC token authority) |
config | | Config PDA |
usdc_mint | | Must match config.usdc_mint |
from_token | | Payer’s USDC associated token account (authority = from) |
recipient_token | | Recipient’s USDC associated token account |
platform_token | | Optional — omit for payments without a platform fee |
treasury_token | | USDC token account owned by config.treasury |
token_program | | SPL Token / Token-2022 program |
The settlement transaction does not create any token accounts. The payer,
recipient, platform (if used), and treasury USDC associated token accounts
must already exist, or settlement fails. The backend signer pays only the
transaction fee, never rent.
Fee math
Identical to the EVM facilitator’s _settleAndDistribute:
platform_fee = value * platform_fee_bps / 10_000 (0 if no platform account)
remainder = value - platform_fee
treasury_fee = remainder * treasury_fee_bps / 10_000 (skipped if platform == treasury)
net_amount = remainder - treasury_fee (→ recipient)
Both fees are capped at MAX_FEE_BPS = 1000 (10%). Multiplication is widened to
u128 so it cannot overflow. Each settlement emits an InstantSettlement
event with the full breakdown:
InstantSettlement {
from, // payer wallet
recipient, // recipient token account
platform, // Option<platform token account>
nonce, // [u8; 32] — x402 payment correlation id
gross_amount, // value
platform_fee,
treasury_fee,
net_amount, // credited to the recipient
}
Replay protection & the nonce
Replay protection is transaction-level, matching the reference x402 SVM
scheme. The paying user signs the settlement transaction itself, which embeds a
recent blockhash: the runtime rejects duplicate signatures, and the transaction
expires with the blockhash. Settling the same payment again would require a
fresh signature from the payer.
No nonce account is created on-chain, so no rent is locked per payment. The
32-byte nonce argument is carried into the InstantSettlement event for x402
payment correlation, and the facilitator additionally refuses to co-sign a nonce
it has already settled (a settlement cache in the payments table).
Errors
| Error | Meaning |
|---|
Paused | The settlement entrypoint is paused |
Unauthorized | Caller is not the config authority / backend signer |
FeeTooHigh | A fee exceeds MAX_FEE_BPS (10%) |
InvalidValue | value (or net after fees) is not greater than zero |
AuthorizationNotYetValid | now <= valid_after |
AuthorizationExpired | now >= valid_before |
InvalidMint | Mint does not match config.usdc_mint |
(Treasury-token ownership is enforced by an Anchor account constraint, which
surfaces as a generic constraint error rather than a program-specific code.)
How settlement is validated
The client builds and payer-signs the settlement transaction; the facilitator’s
POST /v1/settle acts as the co-signing safety boundary. Before it adds the
backend signer’s signature and submits, it checks that the transaction does
exactly one thing the backend signer is willing to pay for:
- the fee payer is the backend signer, and the transaction contains exactly one
transfer_with_authorization instruction against the Meridian program
payer (account 0) is the backend signer; from (account 1) is a different signer
config is the expected Config PDA and the token program is the SPL Token program
- the mint equals
paymentRequirements.asset, and value equals maxAmountRequired
- the validity window is still open (
valid_after <= now, valid_before >= now + 6s)
from_token, recipient_token, and any platform_token are the correct
associated token accounts for the resolved recipient / platform, and
platform_fee_bps matches the organization’s configured fee
- the payer’s wallet signature is present and valid, and the backend signer has
not already signed
Because the program only moves USDC with the payer’s signature and never creates
accounts, a structurally valid transaction means the backend signer risks only
the transaction fee. The recipient (and platform, in platform mode) is resolved
from the caller’s Meridian organization settings, not blindly trusted from
the request.
Deliberately omitted (vs. EVM V6)
Settlement is instant to every party, so there are no fee-accrual balances or
withdraw functions. Also out of scope for this version, all of which remain
EVM-only for now:
- MRDN cashback
- Cross-chain (Across) settlement
- Batch payments
Building & running locally
The program lives in contracts-sol/ in the Meridian
monorepo. Requires the Solana CLI (Agave) and Anchor 0.32.1 (avm use 0.32.1).
cd contracts-sol
anchor build # compile + generate the IDL (target/idl/meridian_x402.json)
anchor test # spin up a local validator and run the test suite
The test suite covers the exact fee splits (with and without a platform fee),
nonce replay rejection, the validity window, the fee cap, a missing payer
signature, the treasury-as-platform skip, pause / unpause, and admin access
control. See contracts-sol/README.md and contracts-sol/SETUP.md for
deploying to devnet and running the facilitator + pay app against it.
References