Skip to content

Repository files navigation

x402_swapper

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

⚠️ WARNING ⚠️

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).


Project structure

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.


Prerequisites

1. Install

npm install

If you hit the @rollup/rollup-darwin-arm64 error:

npm install @rollup/rollup-darwin-arm64 --save-optional

2. Configure environment

cp .env.example .env

Required fields

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.

Optional fields (defaults shown)

Field Default Notes
OPERATOR_MARGIN_BPS 30 (0.3%) Operator fee in basis points. Range 0500 (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 01000 (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.

Critical note: asset ID format

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.

Critical note: TOKEN_NAME

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"

3. Verify your API key

ONE_CLICK_JWT="your-jwt" npm run verify-key

Sends a dry quote, a real quote, and a status request. Exits with code 0 on success.

4. Start the gateway

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.

5. Try the 402 flow

Quick check with curl

# 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).

End-to-end with the test client

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.ts

The 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.

6. Run the test suite

# 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 lint

Buyer query format

The 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:

  1. Zod schema (src/http/swap-input.ts) — structural shape, regex patterns. Failures → 400 INVALID_INPUT with field-level details.
  2. Chain-format cross-check (validateBuyerDestination in src/payment/quote-engine.ts) — e.g. EVM-format destinationAddress is rejected for a NEAR-native destinationAsset. Failures → 400 INVALID_INPUT with structured reasons[].

Unknown chain prefixes pass through (1CS may know chains we don't); the destination-format check skips when it can't resolve the chain.

Destination chain examples

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

Settlement receipt (in the PAYMENT-RESPONSE header)

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.

Discovery surfaces (for x402scan and indexers)

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.


Who would run this?

(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=10 around 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.


Documentation

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)

License

MIT.

About

This repo contains an implementation of a x402 compliant crosschain swap service. The crosschain swap (enabled by 1 Click Swap API) is the x402 resource offered by the merchant, where as the settlement address is the buyer address

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages