Skip to content

feat(x402): first-class Solana (SVM) support — partial-signed tx, fee payer, verify/settle#30

Merged
tkorkmazeth merged 4 commits into
niceberginc:mainfrom
boymak:feat/solana-x402-svm-rail
Jun 25, 2026
Merged

feat(x402): first-class Solana (SVM) support — partial-signed tx, fee payer, verify/settle#30
tkorkmazeth merged 4 commits into
niceberginc:mainfrom
boymak:feat/solana-x402-svm-rail

Conversation

@boymak

@boymak boymak commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Solana (SVM) support to the existing x402 rail — no new rail type. x402 is EVM-rooted (EIP-3009 + EIP-712), but the SVM "exact" scheme is fully supported by facilitators; the rail was already chain-agnostic on verify/settle, so the real work was the client authorization and a couple of requirement fields.

EVM vs Solana, what actually differs:

EVM (existing) Solana / SVM (this PR)
Authorization EIP-3009 transferWithAuthorization, EIP-712 sig client builds + partial-signs a real SPL transfer
Gas none client holds no SOL; facilitator is fee payer
Uniqueness nonce in authorization Memo instruction (random nonce / seller memo)
x402 version 1 2 (SVM exact is a v2 scheme)
Settlement facilitator /settle facilitator co-signs fee-payer slot, submits

Changes

Railsrc/rail-adapters/x402-rail.ts

  • X402RailConfig.feePayer; injected as extra.feePayer for Solana challenges (the client needs it to leave the fee-payer slot empty)
  • x402Version auto-defaults to 2 for SVM networks
  • discoverFeePayer() — pulls the fee payer from the facilitator's GET /supported and caches it

Client signerexamples/x402-solana-recovery/sign-payload.mjs

  • buildSolanaPaymentPayload(): ComputeBudget(limit) + ComputeBudget(price ≤ 5) + TransferChecked + Memo(nonce), partial-signed, base64 → x402 v2 payload
  • @solana/web3.js + @solana/spl-token are dynamically imported (dev-only) so the core SDK install stays light

Docsexamples/x402-solana-recovery/NOTES.md (end-to-end flow, facilitator options: PayAI / Coinbase CDP / self-hosted Kora) + README status row.

Tests (verify & settle separate; success AND failure each)

  • src/__tests__/x402-solana-rail.test.mjs — challenge shape (mint/payTo/v2/feePayer), fee-payer discovery, and verify success / verify reject / verify non-200 / settle success / settle success:false / settle network-error — facilitator stubbed via a fake fetch, no network.
  • src/__tests__/x402-solana-sign.test.mjsoffline signer: payload is x402 v2, fee-payer signature slot is empty (partial sign), serialized tx carries the 4 expected instructions.

Full suite: 228 passing, typecheck clean.

Notes / follow-ups

  • Mainnet settlement against a live facilitator (PayAI / CDP) still needs a manual devnet→mainnet validation pass (kept out of CI — no network).
  • pay.sh intentionally not integrated here; it sits on top of x402 and can be a later layer.

🤖 Generated with Claude Code

boymak and others added 4 commits June 25, 2026 00:26
… payer, verify/settle

Adds Solana to the existing x402 rail without a new rail type. EVM and Solana
differ only in client authorization (no EIP-3009 — the client builds and
partially signs a real SPL transfer, leaving the fee-payer slot for the
facilitator) and a couple of requirement fields.

Rail (src/rail-adapters/x402-rail.ts):
- X402RailConfig.feePayer + extra.feePayer injected for Solana challenges
- x402Version auto-defaults to 2 for SVM (the "exact" scheme is a v2 scheme)
- discoverFeePayer(): pulls the fee payer from the facilitator's /supported

Client signer (examples/x402-solana-recovery/sign-payload.mjs):
- buildSolanaPaymentPayload(): ComputeBudget(limit) + ComputeBudget(price<=5) +
  TransferChecked + Memo(nonce), partial-signed, base64 -> x402 v2 payload
- @solana/web3.js + @solana/spl-token dynamically imported (dev-only)

Tests (verify and settle exercised separately, success AND failure each):
- x402-solana-rail.test.mjs: challenge shape, fee-payer discovery,
  verify/settle success + failure paths (facilitator stubbed via fake fetch)
- x402-solana-sign.test.mjs: offline signer — payload is v2, fee-payer slot
  empty (partial sign), tx carries the 4 expected instructions

Full suite: 228 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…evnet runner

Ran the full rail end-to-end against PayAI + Solana devnet and corrected the
client signer to match the live SVM "exact" v2 wire format. The rail's own
verify/settle bodies were already correct — fixes are client-side only.

Confirmed devnet settle (err: None), fee paid by the facilitator's fee payer
(not the client): tx 5JeSK1je6xrt3HouPUSKheawqiwJhVPtSufqyzNgyqCLBKSZ11Krvty...

Signer (examples/x402-solana-recovery/sign-payload.mjs):
- PaymentPayload now embeds the agreed requirement under `accepted`, with the
  amount as an atomic STRING field `amount` (facilitator rejected the old shape)
- compute-unit limit default 120k -> 30k (facilitators reject too-high limits;
  too-low fails simulation), price still clamped to <= 5

Add examples/x402-solana-recovery/devnet-settle.mjs — self-transfer devnet smoke
test (discover feePayer -> challenge -> partial-sign -> /verify -> /settle),
prints the explorer link. Local payer keypair path is gitignored.

Tests updated for the new payload envelope. Full suite: 228 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beyond the self-transfer smoke test, ran a real payer -> recipient transfer on
devnet via PAY_TO: moved exactly 0.001 USDC (payer 20 -> 19.999, recipient
20 -> 20.001), gas paid by the facilitator, err: None
(tx de6S852jpFTJ1hHLNMBPAaWqrkkMXzjq8XqPbbCf3LD4s1Mm6ZDAU3ah6dxUZueyN19U...).

Also documents that the SVM "exact" fixed instruction layout means the
recipient's token account must exist before settle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m-rail

# Conflicts:
#	.gitignore
#	src/rail-adapters/x402-rail.ts
@tkorkmazeth tkorkmazeth merged commit 0867c47 into niceberginc:main Jun 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants