Skip to content

Quick Start

Get PayWarden running locally in under 3 minutes.

Prerequisites

  • Docker & Docker Compose
  • A TRON wallet (for your hot wallet address)
  • A TronGrid API key (free at trongrid.io)

1. Clone and configure

bash
git clone https://github.com/paywarden/paywarden
cd paywarden
cp .env.example .env

Edit .env with your values. All secrets must be generated — never copy these examples verbatim:

bash
# Generate a 32-byte hex key (run three times for VAULT_KEY / HMAC_SECRET / JWT_SECRET)
openssl rand -hex 32
dotenv
# .env — required
DATABASE_URL=postgresql://paywarden:localdev123@postgres:5432/paywarden
REDIS_URL=redis://redis:6379

# Security (all required — Zod validates on startup, missing = crash)
VAULT_KEY=<64 hex chars>                 # encrypts your seed phrase
API_KEY=<any string ≥ 10 chars>          # bootstrap API key
WEBHOOK_SECRET=<any string ≥ 10 chars>   # HMAC signing key for webhooks
HMAC_SECRET=<64 hex chars>               # internal HMAC for merchant API key storage

# Admin Dashboard (required)
ADMIN_PASSWORD=<min 8 chars>
JWT_SECRET=<64 hex chars>

# TRON (required)
TRONGRID_API_KEY=<your-trongrid-key>
TRON_NETWORK=nile                        # use `mainnet` for production
USDT_CONTRACT=TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf  # Nile testnet USDT

See .env.example for the full template including optional fund-sweeping and alerting variables.

2. Start infrastructure and run migrations

The app container does not run Drizzle migrations on startup. You must migrate an empty database before starting the app, otherwise the default-merchant seed step on boot will crash against missing tables.

bash
# Start Postgres + Redis first
docker compose up -d postgres redis

# Run migrations (host-side, requires pnpm install)
pnpm install
pnpm db:migrate

# Now start the full stack
docker compose up -d

Wait a few seconds for the app to come up, then hit http://localhost:3000/api/v1/health/live to confirm it's alive.

3. Bootstrap options

On first boot with an empty database, PayWarden automatically seeds a "Default Merchant" whose API key equals env.API_KEY (see src/db/seed-default-merchant.ts). That means you have two valid ways to get a usable API key:

  • Quick path (recommended for first-time users): just use the API_KEY value from your .env as the X-API-Key header and jump straight to step 3c (initialize the HD wallet). The seed runs automatically on first boot, so you can skip 3a and 3b entirely.
  • Full admin path: log in as admin, create additional merchants via the admin dashboard or API, and use one of those merchant API keys. Use this path when you need multiple merchants or want to rotate the bootstrap key. Steps 3a / 3b below walk through this.

3a. Log in as admin (full admin path only):

bash
curl -X POST http://localhost:3000/api/v1/admin/auth/login \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{"password":"<ADMIN_PASSWORD from .env>"}'

3b. Create the first merchant (returns your real API key, shown once):

bash
curl -X POST http://localhost:3000/api/v1/admin/merchants \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{"name":"default","webhook_secret":"<same value as WEBHOOK_SECRET in .env>"}'

Response contains api_keycopy it now, it will never be shown again.

3c. Initialize the HD wallet using that API key:

bash
curl -X POST http://localhost:3000/api/v1/wallet/init \
  -H "X-API-Key: <API_KEY from .env or step 3b>" \
  -H "Content-Type: application/json" \
  -d '{}'

Save the mnemonic phrase that's returned — this is your only backup. It will never be shown again.

json
{
  "message": "Wallet initialized. BACKUP YOUR MNEMONIC!",
  "mnemonic": "abandon ability able about above absent absorb abstract ...",
  "first_address": "TXxx...your-first-address"
}

Back up your mnemonic

Store your mnemonic phrase offline in a secure location. It's the only way to recover your funds if the server is lost.

4. Create a payment order

bash
curl -X POST http://localhost:3000/api/v1/payments \
  -H "X-API-Key: <API_KEY from .env or step 3b>" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "order_123",
    "amount": "10.00",
    "currency": "USDT",
    "callback_url": "https://your-backend.com/webhooks/paywarden"
  }'

Response:

json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "external_id": "order_123",
  "address": "TYxx...unique-payment-address",
  "amount": "10.00",
  "currency": "USDT",
  "network": "nile",
  "status": "pending",
  "expires_at": "2026-01-01T01:00:00Z",
  "created_at": "2026-01-01T00:00:00Z",
  "reused": false
}

5. Customer pays

Share the address with the customer, or direct them to the built-in checkout page at http://localhost:3000/checkout/<order-id> (using the id from the create response).

6. Receive webhook confirmation

When payment is confirmed, PayWarden POSTs to your callback_url with a flat payload:

json
{
  "event": "payment.confirmed",
  "order_id": "550e8400-e29b-41d4-a716-446655440000",
  "external_id": "order_123",
  "amount_expected": "10.00",
  "amount_received": "10.00",
  "currency": "USDT",
  "network": "nile",
  "tx_hash": "abc123...",
  "from_address": "TPayer...",
  "to_address": "TYxx...unique-payment-address",
  "status": "confirmed",
  "timestamp": "2026-01-01T00:05:00Z"
}

Verify the signature:

typescript
import crypto from 'crypto'

function verifyWebhook(body: string, signature: string, secret: string): boolean {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex')
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}

// In your webhook handler:
const sig = req.headers['x-signature']
if (!verifyWebhook(req.rawBody, sig, process.env.WEBHOOK_SECRET)) {
  return res.status(401).send('Invalid signature')
}

Next steps

Released under the BSL 1.1 License.