An x402-gated cross-chain swap service. Buyers pay USDC on Base; the gateway routes funds via NEAR Intents 1Click Swap to any of 32+ destination chains (EVM, NEAR, Solana, Stellar, Bitcoin, …) at a buyer-supplied address. One signed EIP-3009 authorization per swap. No wallet-connect dance, no operator-side custody, x402-discoverable.
GET /api/swap?destinationAsset=...&destinationAddress=alice.near&amountIn=10000000
│
▼ 402 + PAYMENT-REQUIRED (deposit address + amount)
│
▼ buyer signs EIP-3009 authorization to the deposit address
│
▼ retry with PAYMENT-SIGNATURE
│
▼ gateway broadcasts on Base, polls 1CS, returns 200 + PAYMENT-RESPONSE
(settlement receipt: destination tx hashes, slippage, operator fee, ...)
1CS deducts the operator fee (appFees) and credits OPERATOR_FEE_RECIPIENT
Token flow: Buyer (USDC on Base) → 1CS Deposit Address → cross-chain swap → Buyer's destination address
This project is in alpha. See docs/TODO.md for what's still missing for a live deployment — most importantly TLS, file-based persistence, graceful shutdown, and a regulatory review (this kind of service is, in many jurisdictions, a regulated money-transmission activity — see docs/OPERATOR_GUIDE.md).
x402_swapper/
├── src/
│ ├── server.ts # HTTP entry point
│ ├── index.ts # Library barrel export (no runtime side effects)
│ ├── types.ts # Shared types — SwapState, SwapRequestInput, errors
│ ├── e2e.test.ts # HTTP protocol compliance tests
│ ├── live-1cs.test.ts # Live 1CS API tests (gated by ONE_CLICK_JWT)
│ ├── server.test.ts # CORS + helmet + discovery endpoint tests
│ ├── payment/ # x402 payment pipeline
│ │ ├── quote-engine.ts # Validate inputs → 1CS quote → margin → x402 PaymentRequirements
│ │ ├── verifier.ts # EIP-3009 / Permit2 signature verification
│ │ ├── settler.ts # Broadcast + 1CS notify + status poll + receipt builder
│ │ └── chain-prefixes.ts # NEP-141 chain metadata + recipient helpers
│ ├── http/ # Express wiring + discovery surfaces
│ │ ├── middleware.ts # x402 middleware (parse query → 402 / verify / settle)
│ │ ├── protected-routes.ts # Single-source registry — only GET /api/swap lives here
│ │ ├── swap-input.ts # Buyer query Zod validator + JSON Schema mirror
│ │ ├── discovery.ts # /.well-known/x402 document builder
│ │ ├── openapi.ts # /openapi.json document builder
│ │ ├── ownership-proof.ts # x402scan EIP-191 canonical message + helpers
│ │ └── cors-options.ts # CORS + helmet option builder
│ ├── storage/
│ │ └── store.ts # SQLite + in-memory state store; D12 stale-DB fail-fast
│ ├── infra/ # Runtime infrastructure
│ │ ├── config.ts # Zod-validated env config (no merchant fields)
│ │ ├── rate-limiter.ts # Per-IP quote limits, settlement cap, quote GC
│ │ └── provider-pool.ts # RPC provider rotation + failover
│ ├── client/ # Buyer-side client library
│ │ ├── x402-client.ts # X402Client (requestResource / submitPayment / payAndFetch)
│ │ ├── signer.ts # EIP-3009 & Permit2 signing
│ │ └── types.ts # Client-side type definitions
│ └── mocks/ # Test fixtures (barrel-exported via mocks/index.ts)
├── scripts/
│ ├── test-client.ts # CLI test client (dry-run / live, env-driven)
│ ├── verify-api-key.ts # 1CS JWT verification
│ ├── generate-ownership-proof.ts # x402scan ownership-proof signer CLI
│ └── test-1cs-quote.sh # Shell script for raw quote testing
├── docs/
│ ├── TODO.md # Production-readiness checklist
│ ├── USER_GUIDE.md # Buyer-facing usage guide
│ ├── OPERATOR_GUIDE.md # Operator regulatory + ops guide
│ └── Facilitator_keys_guidance.md # Facilitator wallet key management
├── .env.example # Environment variable template
├── implementation_plan.md # Swap-mode pivot execution log
├── SWAP_AS_RESOURCE.md # Original product brief
├── CLAUDE.local.md # AI agent onboarding guide
└── package.json
Tests live next to their source (src/payment/quote-engine.test.ts, src/http/middleware.test.ts, etc.); e2e.test.ts and live-1cs.test.ts are system-level by design.
- Node.js >= 20 and npm
- A 1CS JWT token — request one at https://docs.near-intents.org (needed for non-dry quotes)
- For full payment testing:
- A buyer wallet funded with USDC on Base
- A facilitator wallet funded with ETH on Base (for gas — see docs/Facilitator_keys_guidance.md)
npm installIf you hit the @rollup/rollup-darwin-arm64 error:
npm install @rollup/rollup-darwin-arm64 --save-optionalcp .env.example .env| Field | Format | Example |
|---|---|---|
ONE_CLICK_JWT |
JWT string | eyJhbGciOiJS... |
ORIGIN_NETWORK |
CAIP-2 string | eip155:8453 |
ORIGIN_ASSET_IN |
NEP-141 asset ID | nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near |
ORIGIN_TOKEN_ADDRESS |
EVM address | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
ORIGIN_RPC_URLS |
Comma-separated URLs | https://mainnet.base.org,https://base.drpc.org |
FACILITATOR_PRIVATE_KEY |
EVM private key (0x + 64 hex) | 0x59c6995e... |
GATEWAY_REFUND_ADDRESS |
EVM address | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 |
OPERATOR_FEE_RECIPIENT |
NEAR Intents account | treasury.near (required iff OPERATOR_MARGIN_BPS > 0) |
There are no merchant-side fields — buyers supply destination per-request as query params.
| Field | Default | Notes |
|---|---|---|
OPERATOR_MARGIN_BPS |
30 (0.3%) |
Operator fee in basis points. Range 0–500 (1CS rejects appFees above 5%). 1CS deducts this from the swap (via appFees) and credits OPERATOR_FEE_RECIPIENT on NEAR Intents. Surfaced in extra.crossChain.operatorFee. 0 disables the fee entirely. |
STORE_FILE_PATH |
unset (in-memory) | Path to the SQLite file backing the swap-state store. Leave unset for development; set for any real deployment to survive crashes/deploys. D12 stale-DB fail-fast applies — see docs/OPERATOR_GUIDE.md § "First boot". |
STORE_SAVE_INTERVAL_MS |
30000 (30 s) |
How often the SQLite snapshot is flushed to disk. Only used when STORE_FILE_PATH is set. Also flushes on graceful shutdown. |
SHUTDOWN_GRACE_MS |
30000 (30 s) |
On SIGTERM/SIGINT, max wait for in-flight settlements to drain before forcing exit. Align with your k8s terminationGracePeriodSeconds / systemd TimeoutStopSec. |
MAX_AMOUNT_IN |
unset (no cap) | Optional digit-string cap on the buyer's amountIn (origin asset's smallest unit). When set, requests above the cap are rejected 400 INVALID_INPUT before contacting 1CS. Bounds per-request economic exposure and preserves JWT quota. |
SLIPPAGE_TOLERANCE_BPS |
50 (0.5%) |
Slippage tolerance forwarded to 1CS on every quote. Range 0–1000 (10%). Tighten for stablecoin-only pairs, widen for volatile destinations. Surfaced on 402s as extra.crossChain.slippageToleranceBps. |
ALLOWED_ORIGINS |
unset (reflect any) | CORS allowlist for browser clients |
PUBLIC_BASE_URL |
unset | Required before registering on x402scan |
OWNERSHIP_PROOFS |
empty | Comma-separated EIP-191 proofs (use npx tsx scripts/generate-ownership-proof.ts) |
All other tuning knobs (rate limits, poll intervals, token metadata) have safe defaults — see .env.example for the full annotated list.
The 1CS API uses nep141: prefixed asset IDs:
nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near # USDC on Base (origin)
nep141:arb-0xaf88d065e77c8cc2239327c5edb3a432268e5831.omft.near # USDC on Arbitrum (destination example)
nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1 # USDC on NEAR (implicit account)
The old base:0x... and near:nUSDC short forms are not accepted. Verify by calling GET https://1click.chaindefuser.com/v0/tokens or running ./scripts/test-1cs-quote.sh.
The EIP-712 domain name for USDC on Base is "USD Coin", not "USDC". If this is wrong, signature verification fails. Verify with Foundry's cast:
cast call 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 "name()(string)" --rpc-url https://mainnet.base.org
# Returns: "USD Coin"ONE_CLICK_JWT="your-jwt" npm run verify-keySends a dry quote, a real quote, and a status request. Exits with code 0 on success.
npx env-cmd npx tsx src/server.ts(env-cmd handles env vars with spaces like TOKEN_NAME=USD Coin correctly.)
You should see:
[x402-1CS] Loading configuration...
[x402-1CS] Config OK — network=eip155:8453, originAsset=nep141:base-0x833589f..., operatorMarginBps=30
[x402-1CS] State store initialized (SQLite in-memory)
[x402-1CS] Provider pool ready — 1 RPC endpoint(s)
[x402-1CS] Facilitator wallet: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
[x402-1CS] Rate limiting — 20 quotes/60000ms per IP, 10 max concurrent settlements
[x402-1CS] CORS: open (reflect any origin); helmet: enabled
[x402-1CS] Discovery — /.well-known/x402 (...), /openapi.json (OpenAPI 3.1)
[x402-1CS] Mounted 1 protected route(s): GET /api/swap
═══════════════════════════════════════════════════════
x402-1CS Gateway running on http://localhost:3402
Endpoints:
GET /health — health check (no payment)
GET /openapi.json — OpenAPI 3.1 spec (discovery)
GET /.well-known/x402 — x402 resource manifest (discovery)
GET /api/swap — Cross-chain swap (x402)
To test the 402 flow:
curl -i http://localhost:3402/api/swap?destinationAsset=nep141:...&destinationAddress=alice.near&amountIn=10000000
═══════════════════════════════════════════════════════
If env vars are missing or malformed, the gateway exits with a Zod validation error listing exactly which field failed. First boot only: if you have a state.db file from a previous merchant-mode deploy, the D12 stale-DB fail-fast will refuse to boot — delete the file and start fresh.
# Empty query → 400 INVALID_INPUT (Zod validator catches missing fields)
curl -i http://localhost:3402/api/swap
# Valid query → 402 + PAYMENT-REQUIRED header
curl -i 'http://localhost:3402/api/swap?destinationAsset=nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1&destinationAddress=alice.near&amountIn=10000000'The 402 response carries a base64-encoded PAYMENT-REQUIRED header. Decode it to see the deposit address, the amount (exactly your amountIn — the operator fee is deducted from the swap output, not added on top), and the extra.crossChain metadata block (quote ID, expected destination amount, refund target, operator fee breakdown).
The buyer's destination params (chain, asset, recipient, amount, optional refund address) live in a BUYER_INPUTS block at the top of scripts/test-client.ts — edit them inline to match where you want funds to land. The BUYER_PRIVATE_KEY and DRY_RUN toggle stay as env vars (key out of source; run-mode is a CLI choice).
# Dry run (no real payment, no funds needed) — prints the 402 envelope and stops
npx tsx scripts/test-client.ts
# Real payment (requires a funded buyer wallet on Base):
# 1. Edit BUYER_INPUTS in scripts/test-client.ts to point at YOUR destination
# (the default destinationAddress is `buyer.near`, a placeholder — change it)
# 2. Run with the buyer key in env:
DRY_RUN=false BUYER_PRIVATE_KEY=0x... npx tsx scripts/test-client.tsThe test client decodes the PAYMENT-RESPONSE header on success and prints the swap receipt: origin tx hash, destination tx hashes (with explorer URLs), realized slippage, operator fee, and the 1CS correlation ID.
# Mocked tests (~434 tests, no API key needed) — finishes in ~2s
npm test
# Live 1CS API tests (13 tests, gated by ONE_CLICK_JWT)
ONE_CLICK_JWT="your-jwt" npm run test:live
# Type check
npm run typecheck
# Lint
npm run lintThe buyer sends GET /api/swap?... with four fields:
| Param | Required | Format | Meaning |
|---|---|---|---|
destinationAsset |
yes | nep141:... (1CS NEP-141 asset ID) |
What the buyer wants to receive. The destination chain is derived from the asset's prefix (e.g. nep141:arb-... → Arbitrum) |
destinationAddress |
yes | chain-specific (NEAR account, EVM 0x…, Stellar G…, etc.) |
Where to send it |
amountIn |
yes | digit-only positive integer (smallest unit) | What the buyer pays in ORIGIN_ASSET_IN |
refundAddress |
no | EVM address | Refund target if 1CS swap fails. Defaults to cfg.gatewayRefundAddress |
Validation runs in two layers:
- Zod schema (src/http/swap-input.ts) — structural shape, regex patterns. Failures →
400 INVALID_INPUTwith field-level details. - Chain-format cross-check (
validateBuyerDestinationin src/payment/quote-engine.ts) — e.g. EVM-formatdestinationAddressis rejected for a NEAR-nativedestinationAsset. Failures →400 INVALID_INPUTwith structuredreasons[].
Unknown chain prefixes pass through (1CS may know chains we don't); the destination-format check skips when it can't resolve the chain.
The buyer can target any of the 32+ chains supported by the 1CS API (see https://docs.near-intents.org/resources/asset-support). The chain is picked by the destinationAsset prefix — buyers don't pass a separate chain field. Some common assets:
| Chain (derived) | destinationAsset |
destinationAddress format |
|---|---|---|
| NEAR | nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1 |
NEAR account (alice.near) or 64-char hex |
| Arbitrum | nep141:arb-0xaf88d065e77c8cc2239327c5edb3a432268e5831.omft.near |
EVM 0x... |
| Ethereum | nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near |
EVM 0x... |
| Polygon | nep141:polygon-0x3c499c542cef5e3811e1192ce70d8cc03d5c3359.omft.near |
EVM 0x... |
| Optimism | nep141:op-0x0b2c639c533813f4aa9d7837caf62653d097ff85.omft.near |
EVM 0x... |
| Solana | nep141:solana-...omft.near |
Solana public key |
| Stellar | nep141:stellar-...omft.near |
Stellar G... (must have a USDC trustline) |
| Bitcoin | nep141:bitcoin-...omft.near |
Bitcoin address |
The 200 response body is {}. The swap receipt is carried on the PAYMENT-RESPONSE header's extensions.crossChain field — this is x402's standardized extensibility hook (see D14 in implementation_plan.md for the design rationale). Any conforming x402 client / indexer / explorer can read it without route-specific knowledge.
{
"settlementType": "crosschain-1cs",
"destinationTxHashes": [{"hash": "...", "explorerUrl": "https://nearblocks.io/txns/..."}],
"destinationChain": "near",
"destinationRecipient": "alice.near",
"destinationAsset": "nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1",
"destinationAmount": "9985000",
"destinationAmountFormatted": "9.985",
"destinationAmountUsd": "9.99",
"slippage": 0.0015,
"operatorFee": {"bps": 30, "amount": "30000", "currency": "USDC"},
"swapStatus": "SUCCESS",
"correlationId": "corr-..."
}The OpenAPI doc advertises this shape at components.schemas.CrossChainSettlementExtra.
The gateway serves three discovery endpoints, all unauthenticated and rate-limit-exempt:
GET /openapi.json — OpenAPI 3.1 with x-payment-info, x-discovery, x-crosschain
GET /.well-known/x402 — Fan-out resource list + ownership proofs
GET /health — Operator-facing health check (in-flight settlements, RPC status)
To register on x402scan, set PUBLIC_BASE_URL and add ownership proofs (see scripts/generate-ownership-proof.ts). Operator-facing checklist items #11–12 in docs/OPERATOR_GUIDE.md cover registration and /health monitoring.
(See SWAP_AS_RESOURCE.md § 3 for the full discussion. Summary:)
- NEAR-ecosystem onboarding endpoints — wrap a
GET /onramp?chain=near&asset=USDC&recipient=alice.near&amount=10around this and EVM-resident users get NEAR-native USDC in ~30 seconds with one signed authorization. No bridge UI. - Agentic infrastructure — agents transacting cross-chain make one HTTP call instead of scripting a bridge.
- Game / dApp economy operators — players need destination-chain assets to play; one paid endpoint replaces a bridge integration.
- Wallet / SDK providers — "one-click cross-chain top-up" inside the wallet UI.
- Generic "swap-as-an-API" service — public x402-gated swap utility (with the regulatory caveats in docs/OPERATOR_GUIDE.md).
The differentiation versus existing bridges (LiFi / Across / Stargate / etc.) is: single signed authorization, no wallet-connect dance, no operator-side custody during swap, x402-discoverable, no bilateral integration. Worth most when the consumer is an agent or a script.
| Doc | Purpose |
|---|---|
| docs/USER_GUIDE.md | Buyer-facing usage guide — full curl walkthrough, error decoding, receipt parsing |
| docs/OPERATOR_GUIDE.md | Operator-facing — regulatory considerations, KYC/sanctions/geofencing, refund flow, margin guidance, first boot |
| docs/Facilitator_keys_guidance.md | Facilitator wallet key management |
| docs/TODO.md | Production-readiness checklist with priorities |
| implementation_plan.md | Swap-mode pivot execution log (Phases 1–14) |
| SWAP_AS_RESOURCE.md | Original product brief (historical, pre-pivot) |
| CLAUDE.local.md | AI agent onboarding guide (file map, design patterns, invariants) |
MIT.