Server-verified on Arc·Arc Testnet·Chain 5042002

Settlement before status.

A payment is only trustworthy if its paid state corresponds to a real on-chain settlement. OneLink Collect enforces one principle — a server decodes and matches the event emitted by the OneLinkCollect contract before it writes any final record. The client can request a state change; only the chain can justify it. Every receipt anchors to a verifiable Arc transaction.

Settlement contract

0x9b7D5B4DAD4c9B1065908FA8C6C34d697E3cBD0c
View on Arcscan

USDC (ERC-20)

0x3600000000000000000000000000000000000000

Chain ID

5042002
01

Abstract

A payment is only trustworthy if its paid state corresponds to a real on-chain settlement. OneLink enforces this with a single principle — settlement before status — implemented as a server that decodes and matches the on-chain event emitted by the OneLinkCollect contract before it writes a paid (or cancelled) record.

The client can request a state change; only the chain can justify it. Every receipt therefore anchors to a verifiable Arc transaction, and every claim on this page maps to a hash you can re-check on Arcscan.

02

Settlement before status

The design principle holds across the whole product:

  • The front-end never self-certifies a payment.
  • An API route under /api/payments/* fetches the transaction receipt from Arc, decodeEventLogs it against the contract ABI, and confirms the event arguments match the link's expected values before persisting.
  • This holds for three transitions: invoice creation, final paid, and final cancelled.

There is no code path in which the UI alone can fabricate a settlement.

03

System architecture

Four layers, with the contract — not the browser — as the source of truth.

Creator wallet ──signs createLink──▶ Arc (OneLinkCollect)
        │                                 │ emits PaymentLinkCreated
        ▼                                 ▼
  /api/payments/create ──verifies──▶ Supabase (payment_links)
        │                                 [demo mode → localStorage]
Payer wallet ──approve + payLink / bridge+settle──▶ Arc
        │                                 │ emits PaymentCompleted
        ▼                                 ▼
  /api/payments/reconcile ──verifies──▶ status = paid
        │
        ▼
  /receipt/[id] ── renders the verified Arcscan tx
  • Client — Next.js 15 App Router, React 19. Server components by default; wallet flows are client components on wagmi/viem/RainbowKit.
  • Settlement — the OneLinkCollect contract on Arc Testnet holds authoritative link state and emits the events the server verifies.
  • Verification & persistence — Vercel serverless API routes verify events and write state with a service-role client; RLS prevents unauthenticated tampering.
  • Demo mode — with no contract or Supabase configured, the app runs from localStorage with 0xDEM0… pseudo-hashes, explicitly labeled and never used in production.
04

The OneLinkCollect contract

Solidity ^0.8.28 (MIT), built and tested with Foundry (optimizer_runs 200), settling USDC via IERC20.transferFrom.

Functions

createLink(linkId, recipient, amount, expiresAt)
Register an invoice link.
payLink(linkId)
Pay a registered invoice link.
payRecipient(paymentId, recipient, amount)
Profile (handle) payment to a recipient.
cancelLink(linkId)
Creator-only cancellation of an open link.
getLink(linkId) view
Read link state.

Events the server verifies

  • PaymentLinkCreated(linkId, creator, recipient, amount, expiresAt)
  • PaymentCompleted(linkId, payer, recipient, grossAmount, feeAmount)
  • PaymentLinkCancelled(linkId, creator)

Fee model (hard-capped)

  • feeBps is bounded — the constructor and setFeeConfig revert FeeTooHigh if feeBps > 100, a protocol-enforced 1% maximum (≤100 bps).
  • Fee math: feeAmount = (amount * feeBps) / 10_000, deducted at settlement. PaymentCompleted carries both grossAmount and feeAmount.
27 / 27Foundry tests pass≤ 1%fee, capped on-chain11custom-error invariants

Invariants enforce unique link ids, creator-only cancellation, no double-pay, expiry handling, valid recipient/amount, the fee cap, and a checked token transfer (NotCreator, LinkAlreadyPaid, FeeTooHigh, TransferFailed, and others). Link identity is keccak256("onelink:" + slug), re-derived and matched server-side so a forged invoice cannot be persisted.

05

Server-verified settlement model

/api/payments/create is representative of the verify-then-write pattern used by every money-touching route:

  1. 1Require HAS_CONTRACT + Supabase env, else return 503 (demo mode handles this client-side via localStorage).
  2. 2Build a viem createPublicClient({ transport: http(ARC_RPC_URL) }).
  3. 3Fetch the submitted transaction receipt and decodeEventLog it against oneLinkCollectAbi.
  4. 4Match the decoded PaymentLinkCreated args against the request and the URL-derived link id.
  5. 5Only then upsert into the Supabase payment_links table with the service-role client.

reconcile and cancel apply the same pattern for the final paid and cancelled states. All routes are rate-limited (e.g. create is 20 requests / 60s) and return generic, non-leaking error messages.

06

Payment routes

Arc-direct

Live-proven

Payer holds USDC on Arc. Two transactions: approve then payLink(linkId). The server verifies PaymentCompleted before marking paid.

Bridge · Circle CCTP + App Kit

Live-proven

Native USDC is burned on the source and minted on Arc, then settled in the same flow — surfacing approve → burn → fetchAttestation → mint. Base Sepolia → Arc is live-proven; Ethereum Sepolia, Arbitrum Sepolia, and Polygon Amoy are beta.

Unified balance · Circle Gateway

Gated

Hand-rolled EIP-712 burn-intents against the Gateway API. Implemented end-to-end but gated behind NEXT_PUBLIC_ENABLE_GATEWAY — disabled in checkout until a funded deposit/burn/mint flow is proven.

07

Arc integration

USDC is Arc's native gas token, so a payer never needs ETH — the same USDC being sent also covers the fee. Constants live in lib/arc.ts:

ARC_CHAIN_ID
5042002
ARC_RPC_URL
https://rpc.testnet.arc.network
ARC_EXPLORER_URL
https://testnet.arcscan.app
ARC_USDC_ADDRESS
0x3600000000000000000000000000000000000000
USDC_DECIMALS
6 (ERC-20); native gas is USDC (18 decimals)
08

Circle integration

  • CCTP · App Kit lib/circle-payments.ts dynamically imports @circle-fin/app-kit + @circle-fin/adapter-viem-v2 and calls kit.bridge(…) with to: { chain: "Arc_Testnet" }, surfacing live step events for the burn-and-mint.
  • Gateway — hand-rolled in lib/gateway.ts (no SDK): EIP-712 burn-intents (domain { name: "GatewayWallet" }) against the testnet Gateway API, Arc destination domain 26. Gated until a funded proof is run.
09

Data & identity

  • Persistence — Supabase for cross-device metadata, gated by server-side verification; a localStorage fallback powers demo mode. Migrations enforce the same invariants from the database side (anonymous standard-invoice insertion is rejected; unpaid profile rows are hidden from the dashboard) — 0 Supabase security advisor lints.
  • Demo modeHAS_CONTRACT / IS_DEMO_MODE derive from the contract env. A production-safety throw blocks silent demo-mode deploys unless NEXT_PUBLIC_ALLOW_DEMO=true.
  • Profile claims — a permanent freelancer handle is claimed with an EIP-712 typed-data signature (domain { name: "OneLink Collect", chainId: 5042002 }, ~600s TTL). The server verifies the signature, binds owner == recipient, enforces freshness, and is rate-limited, so a captured signature is not trivially replayable.
10

Security model

  • Contract — capped fee, custom-error invariants, checked transfers, creator-only cancellation, no double-pay; 27 passing Foundry tests.
  • Server trust boundary — final state requires a verified on-chain event; forged anonymous invoice creation and forged cancellation are rejected (proven in QA).
  • API hardening — per-IP rate limiting on payment/gateway/profile routes; generic error responses with no raw RPC leakage; the Gateway route validates the Arc destination domain.
  • App headersnosniff, X-Frame-Options: DENY, a restrictive Permissions-Policy, and HSTS with preload.
  • Repository — CodeQL with 0 open alerts, secret scanning + push protection, Dependabot, and required status checks on a protected main.
  • AccessibilitymaximumScale: 5; pinch-zoom is preserved (WCAG 1.4.4).
11

Verified scope & limits

Proven on the live deployment:

  • Arc-direct payment and browser-wallet end-to-end.
  • WalletConnect QR pairing and signed Arc payment.
  • Base Sepolia → Arc bridge via Circle CCTP / App Kit.
  • Permanent profile handle and payer-initiated profile payment.
  • Server-verified creator cancellation and failure-state recovery.
  • A 5-viewport visual QA sweep.

Not claimed:

Mainnet
Not in scope; Arc Testnet only.
Solana
Not implemented.
Circle Gateway checkout
Feature-gated; no funded proof yet.
Other bridge sources
Base Sepolia proven; others beta.
Arbitrary-wallet auto-pay
Not claimed.