Skip to content

feat(arc): Arc testnet payment rail with Solana|Arc selector (flagged) - #538

Merged
leocagli merged 5 commits into
mainfrom
feat/arc-rail
Oct 14, 2026
Merged

leocagli merged 5 commits into
mainfrom
feat/arc-rail

Conversation

@leocagli

Copy link
Copy Markdown
Collaborator

What

Phase 1 of the Arc integration plan (docs/arc/PLAN_INTEGRACION_ARC.md in the Open-Stellar workspace), adapted to the owner's decision of 2026-10-14: same repo, same Vercel project and domain, and an in-app selector "Red de pago: Solana | Arc" that each browser chooses. Everything Arc is behind NEXT_PUBLIC_ENABLE_ARC (default off), so production keeps behaving exactly as today until the owner turns it on.

How

  • Rail abstraction (lib/rails): RailInfo for Solana devnet and Arc testnet (CAIP-2, receipt network, label, asset, decimals, explorer links, tx/address validators), requestedRail() (Arc refused while the flag is off) and a per-browser preference (lib/rails/preference.ts, localStorage).
  • x402 on Arc testnet (lib/rails/arc/x402.ts): exact on eip155:5042002 with @x402/evm@2.28.0, USDC 0x3600...0000 (6 decimals), settled by Circle's Facilitator Service. Order kept: verify, settle, then the LLM.
  • Circle facilitator client (lib/rails/arc/facilitator.ts): keyless-trial seller proof (EIP-712 "Circle Facilitator Seller Request" v1 over keccak256 of the exact body, signed by the payTo key), optional CIRCLE_API_KEY Bearer, payment-identifier derived from the EIP-3009 nonce, and settlement_pending handled by polling /status (core 2.28 does not retry a pending result without a tx hash).
  • Never deliver unsettled, never charge twice: still pending answers 409 and is remembered in KV (ac:arc:x402:pending:*); the buyer resends the same signed payment (never re-signs), the retry skips verify and settles once; a settled payment is claimed (ac:arc:x402:settled:*) so a replayed header cannot buy a second task.
  • Arc agents' wallet: separate EVM key in its own sealed cookie (ac_agent_wallet_arc, purpose agent-wallet:arc, envelope v:2, chain:"arc"), never derived from the Solana seed; same browser id, so caps and approvals stay per browser. Balance via ERC-20 balanceOf (6 decimals only); withdraw signed by the agent, leaving a 0.01 USDC gas reserve.
  • Funding: from MetaMask (wagmi injected connector, Arc testnet added with defineChain from the documented RPC/explorer) with a plain USDC.transfer; faucet link to faucet.circle.com.
  • Hires: createArcChatRunDeps pays the task endpoint with ?network=arc; prompts, approval card and receipts say "Arc testnet".
  • Receipts: network: "arc-testnet", 0x hashes accepted (they were dropped before), own list ac:arc:receipts:v1, /api/receipts?network=solana|arc, network filter and per-receipt explorer links in /explorer, network badge in chat receipts. Arc USDC logs are read from the ERC-20 emitter only (the system emitter's 18-decimal duplicate is ignored).
  • viem moves from 2.47.6 to 2.57.5 in the lockfile (required by @x402/evm@2.28.0).

Out of scope (later phases): CCTP from Solana, ERC-8004 on Arc (nothing here blocks it: the payment binding is already stored under ac:arc:x402:paid:*).

Env vars (only needed when the flag is turned on)

NEXT_PUBLIC_ENABLE_ARC=true, X402_ARC_PAY_TO, ARC_SELLER_PROOF_KEY (the key that controls X402_ARC_PAY_TO), optional CIRCLE_API_KEY, X402_ARC_FACILITATOR_URL, ARC_RPC_URL, NEXT_PUBLIC_ARC_RPC_URL, NEXT_PUBLIC_ARC_EXPLORER_URL. Nothing was changed in Vercel.

Checks

  • npx tsc --noEmit: clean.
  • ESLint on touched files: 0 errors.
  • npx vitest run: 185 files, 1372 tests passed (existing suites unchanged and green; 10 new test files: rail registry, Circle facilitator client with mocked HTTP for supported/verify/settle/settlement_pending/failed/403, seller-proof signature recovery, x402 seller+buyer end to end with a fake facilitator (402, pay once, replay refused, pending then single settle on retry), receipt parsing with the two Transfer events filtered by emitter, Arc cookie sealing and isolation from the Solana cookie, withdrawal with gas reserve, Arc chat deps, Arc receipts and network filter, task route with ?network=arc).
  • npm run build + npm run size-limit: 1.37 MB brotlied of 2 MB, with the flag off and with it on (the Arc panel is a lazy chunk).
  • npm run secretlint: clean.
  • Playwright (local, chromium): payment-network, agent-wallet, sidebar-tabs, onboarding-modal specs: 12 passed with NEXT_PUBLIC_ENABLE_ARC=true, 10 passed (2 Arc specs skipped) with the flag off. Network mocked in the new spec.

Testnet end to end

Run locally against next start built with the flag on, keyless trial (no Circle account, no API key), test keys only. LLM calls were mocked in the test harness; the payments were real:

  • Browser Arc agents' wallet created via GET /api/agent-wallet?network=arc: 0xA54f364a8BA1A70fbA0f4191915798E022910984.
  • Funded with 0.05 USDC from the test payer: 0xc7847a53...574ea.
  • Chat with network: "arc": the orchestrator hired a teammate; the hire paid 0.01 USDC agents' wallet to treasury, settled by Circle: 0x3bf24ad0...2066d (block 67117434, status success, ERC-20 Transfer of 10000 base units found by emitter). The receipt showed up in /api/receipts?network=arc and /explorer with the Arc explorer link.
  • Withdraw back to the payer (agent pays its own gas in USDC, 0.01 reserve kept): 0xb7ec4236...cf96a5.

Known gaps

  • A settlement still pending after the 20 s wait answers 409 and is stored in KV; the buyer resends the same payment up to 3 times. There is no cron yet that reconciles pending payments via /status on its own.
  • The approval token is not bound to the network: switching "Red de pago" between an approval card and its approval pays the approved hire on the newly chosen network.
  • The daily cap is shared by both networks (same browser id), which is conservative.
  • The browser pay-per-task panel (PaidTaskPanel) and 8004 identity stay Solana-only; ERC-8004 on Arc is phase 2.
  • Funding from MetaMask was not exercised in a real browser extension (no wallet in CI); the transfer itself was tested from a script with the same call.
  • The keyless trial allowance per payTo is undocumented; when it runs out, Arc payments answer 503 until CIRCLE_API_KEY is set.

🤖 Generated with Claude Code

CosmosPay-org and others added 5 commits October 14, 2026 02:58
Adds a payment rail abstraction (lib/rails) and an Arc testnet rail behind
NEXT_PUBLIC_ENABLE_ARC (default off): x402 exact on eip155:5042002 settled by
Circle's Facilitator Service (keyless-trial seller proof or API key), with
settlement_pending handled so nothing is delivered or charged twice; a
separate per-browser Arc agents' wallet in its own sealed cookie, funded from
MetaMask with a USDC transfer and withdrawn with a gas reserve; receipts with
network arc-testnet in their own list and an /explorer network filter; and
"Red de pago: Solana | Arc" in the Wallet tab. Solana stays the default and
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The explorer intro and API hint mention Arc testnet only while the flag is
on; the Wallet tab's Solana panels load in the same chunk as the selector;
the selector e2e waits for the dev server to compile lazy chunks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Derive the Arc payment-identifier only from signed data (keccak of
  authorization.from + nonce) and always overwrite the client's extension,
  so a forged id can no longer take over a pending payment or slip a
  settled one past the "already used" check.
- Store a hash of the signed payment in the pending record and skip
  verify on retry only when the retried payment matches it exactly.
- Treat a /settle timeout, 5xx or pending without a status id as pending
  (409) so the buyer resends the same payment instead of being charged
  with nothing delivered. Keep the settled mark at least as long as the
  authorization's validBefore.
- Sign the network into approval tokens; an approval raised on Arc is
  refused when approved with Solana selected (and vice versa), and the
  chat sends the card's network, not the current selection.
- Arc wallet panel: offer MetaMask only when a browser wallet exists,
  hide "Retirar todo" when only the gas reserve is left, Spanish errors.
- Document that NEXT_PUBLIC_ENABLE_ARC must be set before the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings new-code coverage back over the SonarCloud gate (80%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@leocagli
leocagli merged commit 3baf1db into main Oct 14, 2026
7 checks 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