Architecture
API Routes
The API surface lives under apps/web/src/app/api as Next.js route handlers. Every request body is validated with zod; every mutation is written through the double-entry ledger.
Request lifecycle
Every request passes the same gauntlet before its handler runs — and money-moving routes add rail pause checks, per-transaction caps, and idempotency on top:
Authentication
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/webauthn | Register / authenticate a passkey (FIDO2) |
| POST | /api/auth/otp/start | Send a phone / email OTP (Twilio Verify) |
| POST | /api/auth/google | Sign in with Google Identity Services |
Cards
| Method | Path | Description |
|---|---|---|
| POST | /api/cards | Issue a 2-of-2 MPC virtual card |
| POST | /api/cards/settings | Freeze, set limits, update controls |
curl -X POST https://furlpay.app/api/cards/settings \
-H "Authorization: Bearer sk_live_…" \
-d '{ "cardId": "card_1a2b", "frozen": true, "spendLimit": 25000 }'Investing & market data
| Method | Path | Description |
|---|---|---|
| POST | /api/investing/order | Fractional stock / ETF order (Alpaca) |
| GET | /api/investing/portfolio | Holdings, cost basis and P/L |
Quotes and fundamentals are sourced from the Twelve Data API; real-time execution and price streams run over Alpaca WebSockets.
Agentic payments (x402)
Furlpay speaks x402 — the HTTP 402 "Payment Required" protocol — so AI agents and API clients pay per-request in USDC with no account or key. An unpaid request returns a signed quote; the client pays and retries with an X-PAYMENT header.
| Method | Path | Description |
|---|---|---|
| GET | /api/x402/fx | Pay-per-call mid-market FX quote ($0.01 USDC) |
# 1. unpaid request → 402 with a payment quote
curl -i https://furlpay.com/api/x402/fx?from=USD&to=EUR&amount=100
# 2. sign the EIP-3009 authorization, then retry with the payment attached
curl https://furlpay.com/api/x402/fx?from=USD&to=EUR&amount=100 \
-H "X-PAYMENT: $(base64_payload)" # → 200 + X-PAYMENT-RESPONSE receiptVerification is hardened against the authorization, binding, replay, and web-layer attack classes documented in 2026 x402 research. See /docs/payments/agentic-x402 for the full flow.
USDC payments & compliance review
The unified USDC payment service settles on Arbitrum One (0.5% fee vs 1.5–3.5% card interchange). The flow is create (quote + EIP-712 sign data) → execute (signature → settle); both legs are OFAC-screened before anything touches the chain.
| Method | Path | Description |
|---|---|---|
| POST | /api/payments/create | Create a USDC payment intent (honors Idempotency-Key) |
| POST | /api/payments/execute | Submit the EIP-712 signature and settle |
| GET | /api/payments/:id | Poll payment status |
| GET | /api/payments/review | Human-in-the-loop review queue (session-gated) |
| POST | /api/payments/review | Approve / reject a paused payment (session-gated) |
Idempotency. create honors an Idempotency-Key header — a retry with the same key returns the same intent (HTTP 200 with replayed: true) instead of minting a duplicate. This is the retry-storm guard: agents that resubmit after a dropped receipt, or fan-outs of sub-agents sharing a coordinated key, never generate a double charge. execute is idempotent too — a retry against an already-settled payment replays the original receipt rather than erroring.
Human-in-the-loop (HITL) compliance gateway. A payment that clears sanctions screening but crosses a regulatory threshold does not settle automatically — it pauses at pending_review and execute returns 202 Accepted. A payment pauses when:
- its value is at or above
FURLPAY_HITL_THRESHOLD_USD(default $3,000), or - either counterparty screens as elevated risk, or
- its
metadata.jurisdictionis inFURLPAY_HIGH_RISK_JURISDICTIONS.
The compliance inbox is notified, and a human officer approves or rejects via /api/payments/review (session-gated, so the decision is always attributable to a person) before funds move on-chain. Agents research and flag; humans make the final call.
# create with an idempotency key (safe to retry verbatim)
curl -X POST https://furlpay.com/api/payments/create \
-H "content-type: application/json" \
-H "Idempotency-Key: sess_9f3c-booking-42" \
-d '{ "amount": 5000, "recipient": "0xabc…", "metadata": { "jurisdiction": "US" } }'
# execute → 202 pending_human_review when it crosses the $3k threshold
# officer approves it:
curl -X POST https://furlpay.com/api/payments/review \
-H "content-type: application/json" \
--cookie "furlpay_session=…" \
-d '{ "paymentId": "pay_…", "decision": "approve" }'Solana Actions & Blinks
Shareable checkout links unfurl into signable payment widgets on X, Discord, and any Blink client.
| Method | Path | Description |
|---|---|---|
| GET | /actions.json | Blink client discovery rules |
| GET | /api/actions/pay/[orderId] | Blink metadata (icon, label, buttons) |
| POST | /api/actions/pay/[orderId] | Return the base64 SOL/USDC transaction to sign |
Webhooks & signature verification
| Method | Path | Description |
|---|---|---|
| POST | /api/webhooks/marqeta | Card processor events |
| POST | /api/webhooks/bridge | On/off-ramp settlement events |
| POST | /api/webhooks/wise | Local bank account events |
Every webhook carries a Furlpay-Signature header. Verify it against your endpoint secret before acting on the payload.
import { Furlpay } from '@furlpay/furlpay-node';
export async function POST(req: Request) {
const raw = await req.text();
const event = Furlpay.webhooks.constructEvent(
raw,
req.headers.get('furlpay-signature'),
process.env.FURLPAY_ENDPOINT_SECRET!,
);
// event is now trusted — handle event.type
}Idempotency
Idempotency-Key header on POSTs so retries never double-issue a card or double-place an order.