A crypto payment gateway for merchants. Accept USDC on Ethereum, Base, and BNB Chain. Funds are automatically swept from deposit addresses to your treasury after each payment.
- Prerequisites
- First-time Setup
- Environment Variables
- Running Locally
- Architecture
- Payment Flow
- Sweep System
- Background Workers
- Project Structure
- Node.js 20+
- pnpm 9+
- Docker (for PostgreSQL and Redis)
- Turnkey account — wallet custody
- Moralis account — blockchain event streaming
1. Install dependencies
pnpm install2. Start infrastructure
docker compose up -dThis starts PostgreSQL on port 5432 and Redis on port 6379.
3. Configure environment
cp apps/api/.env.example apps/api/.envFill in the required values — see Environment Variables below.
4. Run database migrations
cd apps/api
npx prisma migrate dev
npx prisma generate5. Set up Moralis stream
The API registers a Moralis webhook stream on startup to receive blockchain events. You need a publicly reachable URL for the webhook. For local development, use a tunnel (e.g. ngrok):
ngrok http 3002Set STREAM_WEBHOOK_URL=https://<your-ngrok-url>/stream/webhook in your .env.
6. Start the apps
# From repo root — starts all apps in parallel
pnpm dev| App | URL | Description |
|---|---|---|
| API | http://localhost:3002 | NestJS backend + Swagger at /api |
| Dashboard | http://localhost:3000 | Merchant dashboard |
| Checkout | http://localhost:3001 | Hosted payment page |
All variables live in apps/api/.env.
| Variable | Description | Default |
|---|---|---|
NODE_ENV |
development or production |
— |
PORT |
API server port | 3002 |
DATABASE_URL |
PostgreSQL connection string | — |
CORS_ORIGINS |
Comma-separated allowed origins | — |
CHECKOUT_URL |
Base URL of the checkout app | http://localhost:3001 |
| Variable | Description |
|---|---|
JWT_SECRET |
Secret for signing JWTs (min 32 chars) |
JWT_ACCESS_EXPIRES_IN |
Access token TTL (e.g. 15m) |
JWT_REFRESH_EXPIRES_IN |
Refresh token TTL (e.g. 7d) |
| Variable | Description | Default |
|---|---|---|
REDIS_HOST |
Redis host | localhost |
REDIS_PORT |
Redis port | 6379 |
Turnkey manages all private keys — deposit wallets and treasury wallets. No private keys are stored in the database.
| Variable | Description |
|---|---|
TURNKEY_ORGANIZATION_ID |
Your Turnkey org ID |
TURNKEY_API_PUBLIC_KEY |
Turnkey API public key |
TURNKEY_API_PRIVATE_KEY |
Turnkey API private key |
| Variable | Description |
|---|---|
MORALIS_API_KEY |
Moralis API key |
STREAM_WEBHOOK_URL |
Public URL Moralis will POST events to |
STREAM_SECRET |
Secret used to verify Moralis webhook signatures |
Used for gas estimation, transaction broadcasting, and fee queries.
| Variable | Chain |
|---|---|
RPC_ETH |
Ethereum mainnet |
RPC_BASE |
Base mainnet |
RPC_BSC |
BNB Chain mainnet |
RPC_SEPOLIA |
Ethereum Sepolia (test) |
RPC_BASE_SEP |
Base Sepolia (test) |
RPC_BSC_TEST |
BNB Chain Testnet (test) |
| Variable | Description | Default |
|---|---|---|
PAYMENT_TOLERANCE_BPS |
Underpayment tolerance in basis points | 100 (1%) |
PLATFORM_FEE_BPS |
Platform fee in basis points | 100 (1%) |
# All apps
pnpm dev
# Individual
pnpm --filter @faxo/api dev
pnpm --filter @faxo/dashboard dev
pnpm --filter @faxo/checkout devAPI modes: The API differentiates between live and test keys. Test-mode payment intents are routed to testnets (Sepolia, Base Sepolia, BSC Testnet) automatically — no real funds involved.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Dashboard │ │ Checkout │ │ Merchant API │
│ (Next.js :3000) │ │ (Next.js :3001) │ │ (REST/Swagger) │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
└───────────────────────┴─────────────────────────┘
│
┌────────────▼────────────┐
│ NestJS API :3002 │
│ │
│ ┌─────────────────────┐│
│ │ Payment Intents ││
│ │ Stream Ingestion ││
│ │ Sweep Engine ││
│ │ Withdrawals ││
│ │ Webhooks ││
│ └─────────────────────┘│
└────┬──────────┬─────────┘
│ │
┌──────────▼──┐ ┌────▼──────────┐
│ PostgreSQL │ │ Redis │
│ (Prisma) │ │ (BullMQ jobs) │
└─────────────┘ └───────────────┘
│
┌───────────┴───────────┐
│ │
┌────▼──────┐ ┌─────▼──────┐
│ Turnkey │ │ Moralis │
│ (custody) │ │ (streams) │
└───────────┘ └────────────┘
Key design decisions:
- Non-custodial per-payment addresses — each payment intent gets a unique Turnkey HD wallet. The merchant never shares a deposit address across payments.
- Moralis streams — instead of polling the chain, Moralis pushes ERC-20 transfer events to the API via webhook. This covers all 6 supported chains in a single stream.
- EIP-3009 gasless sweeps — USDC on Ethereum and Base uses
transferWithAuthorization. The deposit address signs an off-chain authorization; the treasury broadcasts and pays gas. Deposit addresses never need ETH. - Strategy B ledger — merchant balances are a simple
availablecounter. Payments credit it; withdrawals debit it. No on-chain balance queries needed for the dashboard. - BullMQ for all async work — sweep execution, withdrawal execution, webhook delivery, and expiry checks are all queued jobs with built-in retry and deduplication.
1. Merchant creates payment intent (POST /payment-intents)
→ Turnkey creates a fresh HD wallet → unique deposit address
2. Customer visits checkout page, sends USDC to the deposit address
3. Moralis detects the transfer → POSTs to /stream/webhook
4. Stream service matches the transfer to the intent
→ creates a Payment record
→ advances intent status: pending → confirming → confirmed → finalized
5. Dashboard receives real-time status updates via SSE
6. On finalization:
→ merchant balance is credited (netAmount after fee)
→ sweep is enqueued
→ webhook events are delivered to merchant endpoints
| Status | Meaning |
|---|---|
pending |
Awaiting payment |
confirming |
Transaction seen, waiting for confirmations |
confirmed |
Required confirmations reached |
finalized |
Balance credited, sweep enqueued |
expired |
No payment received within TTL |
failed |
Underpayment or error |
| Chain | Confirmations |
|---|---|
| Ethereum | 3 |
| Base | 1 |
| BNB Chain | 15 |
| Testnets | 1–2 |
After a payment is finalized, the net USDC amount is swept from the deposit address to the merchant's treasury address.
EIP-3009 path (Ethereum, Base):
- The deposit address signs an off-chain
TransferWithAuthorizationmessage (EIP-712, via Turnkey). - The treasury address broadcasts the
transferWithAuthorizationcall and pays ETH gas. - USDC moves from deposit → treasury in a single on-chain transaction.
- The deposit address needs zero ETH.
Fallback path (BSC):
BSC USDC is Binance-bridged and may not support EIP-3009. This path uses a standard ERC-20 transfer call from the deposit address — the deposit address must hold BNB for gas. Not currently funded automatically.
Treasury addresses are lazily provisioned — a Turnkey wallet is created the first time a sweep runs for a given merchant + chain. The address is shown on the Balances page in the dashboard.
Sweep recovery: A cron worker runs every 5 minutes to re-enqueue any sweeps stuck in failed status. Sweeps with invalid deposit addresses (legacy UUID bug) are permanently skipped rather than retried.
All workers run inside the API process via BullMQ queues backed by Redis.
| Worker | Queue | Trigger | Description |
|---|---|---|---|
SweepWorker |
sweep-execution |
On payment finalization | Executes deposit → treasury transfer |
SweepRecoveryWorker |
cron (5 min) | Scheduled | Re-enqueues failed sweeps |
WithdrawalWorker |
withdrawal-execution |
On withdrawal creation | Executes treasury → external transfer |
WebhookDeliveryWorker |
webhook-delivery |
On payment events | Delivers events to merchant webhooks with exponential backoff |
ExpiryWorker |
cron (1 min) | Scheduled | Marks overdue payment intents as expired |
StreamHealthWorker |
cron (5 min) | Scheduled | Verifies Moralis stream is active and covers all chains |
faxo/
├── apps/
│ ├── api/ # NestJS backend
│ │ ├── prisma/ # Schema, migrations
│ │ └── src/
│ │ ├── config/ # chains.ts, app config
│ │ ├── modules/ # Feature modules
│ │ │ ├── auth/
│ │ │ ├── payment-intents/
│ │ │ ├── payments/
│ │ │ ├── stream/ # Moralis webhook ingestion
│ │ │ ├── sweep/ # Sweep execution + controller
│ │ │ ├── turnkey/ # Wallet custody
│ │ │ ├── balances/
│ │ │ ├── withdrawals/
│ │ │ └── webhooks/
│ │ └── workers/ # BullMQ consumers + cron jobs
│ ├── dashboard/ # Next.js merchant dashboard
│ └── checkout/ # Next.js hosted payment page
└── packages/
├── types/ # Shared TS interfaces and enums
├── ui/ # Shared UI components
├── react/ # Shared React hooks/utils
└── config/ # Shared TS/ESLint config