Skip to content

ayorcodes/faxo

Repository files navigation

Faxo

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.


Table of Contents


Prerequisites

  • Node.js 20+
  • pnpm 9+
  • Docker (for PostgreSQL and Redis)
  • Turnkey account — wallet custody
  • Moralis account — blockchain event streaming

First-time Setup

1. Install dependencies

pnpm install

2. Start infrastructure

docker compose up -d

This starts PostgreSQL on port 5432 and Redis on port 6379.

3. Configure environment

cp apps/api/.env.example apps/api/.env

Fill in the required values — see Environment Variables below.

4. Run database migrations

cd apps/api
npx prisma migrate dev
npx prisma generate

5. 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 3002

Set 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

Environment Variables

All variables live in apps/api/.env.

Core

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

Auth

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)

Redis

Variable Description Default
REDIS_HOST Redis host localhost
REDIS_PORT Redis port 6379

Turnkey (wallet custody)

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

Moralis (blockchain events)

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

RPC Endpoints

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)

Fees

Variable Description Default
PAYMENT_TOLERANCE_BPS Underpayment tolerance in basis points 100 (1%)
PLATFORM_FEE_BPS Platform fee in basis points 100 (1%)

Running Locally

# All apps
pnpm dev

# Individual
pnpm --filter @faxo/api dev
pnpm --filter @faxo/dashboard dev
pnpm --filter @faxo/checkout dev

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


Architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│    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 available counter. 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.

Payment Flow

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

Payment Intent Statuses

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

Confirmations required per chain

Chain Confirmations
Ethereum 3
Base 1
BNB Chain 15
Testnets 1–2

Sweep System

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

  1. The deposit address signs an off-chain TransferWithAuthorization message (EIP-712, via Turnkey).
  2. The treasury address broadcasts the transferWithAuthorization call and pays ETH gas.
  3. USDC moves from deposit → treasury in a single on-chain transaction.
  4. 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.


Background Workers

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

Project Structure

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors