Non-custodial, cross-chain payment infrastructure where payers can send from any supported chain and merchants receive on their preferred chain/asset, with Stellar as the settlement hub.
Useroutr is built around atomic HTLC settlement across chains:
- Inbound: payer locks funds on source chain (EVM/Stellar/Starknet routes).
- Routing: Stellar path payment + Soroban contracts handle conversion and fee deduction.
- Outbound: funds are bridged/disbursed to merchant destination chain.
- Guarantee: either both sides complete or funds are refunded after timeout.
Core backend responsibilities (NestJS modular monolith in apps/api):
- Auth + merchants (JWT/API keys)
- Quotes (30s Redis TTL lock)
- Payments lifecycle + webhook events
- Bridge routing (CCTP, Wormhole, Layerswap)
- Relay service (HTLC event watching + secret propagation)
- Invoices, links, payouts, notifications, analytics
Smart contracts:
- Soroban contracts (
contract/soroban): HTLC, settlement, fee collector, escrow - EVM HTLC (
contract/evm/contracts/HTLCEvm.sol) - Starknet HTLC (
contract/starknet/src/htlc.cairo)
apps/api— NestJS API + Prisma schema/migrationspackages/stellar— shared Stellar clients/helperspackages/types— shared chain/payment typescontract/soroban— Rust Soroban contractscontract/evm— Solidity HTLC + Hardhat configcontract/starknet— Cairo HTLC
- Node.js 20+
- npm 10+
- Docker + Docker Compose
openssl(for secret generation)
Optional for contract work:
- Rust toolchain (
wasm32-unknown-unknown) stellarCLI- Hardhat-compatible wallet keys/testnet RPCs
From repo root:
npm installIf API workspace deps are not installed from root in your environment:
cd apps/api
npm install
cd ../..Create .env from .env.example at repo root:
cp .env.example .envImportant local DB note:
docker-compose.ymlmaps Postgres to host port 5434 (5434:5432)- so set
DATABASE_URLto:
DATABASE_URL="postgresql://tavvio:password@localhost:5434/tavvio"Set at least these for local API startup:
DATABASE_URLREDIS_URL=redis://localhost:6379JWT_SECRET
docker compose up -d
docker compose psFrom apps/api:
npx prisma migrate dev
npx prisma generateFrom apps/api:
npm run start:devAPI runs on http://localhost:3000 by default.
Steps 4–6 above start only Postgres and Redis and leave you to run each app by hand. To bring up the whole stack instead:
cp .env.example .env && docker compose up| Service | URL | Notes |
|---|---|---|
api |
http://localhost:3000 | /healthz liveness, /readyz readiness; everything else under /v1 |
dashboard |
http://localhost:3001 | |
www |
http://localhost:3002 | |
checkout |
http://localhost:3003 | |
postgres |
localhost:5434 | tavvio/password, volume pgdata |
redis |
localhost:6379 | volume redisdata |
The repo is bind-mounted into each app container, so edits on the host
hot-reload inside it. node_modules and .next are masked with anonymous
volumes — the host's node_modules is built for the host's platform and would
shadow the image's Linux binaries (Prisma engines, SWC).
api waits for Postgres and Redis to pass their healthchecks before starting;
the frontends wait for api. Migrations are not run automatically:
docker compose exec api npx --workspace=api prisma migrate devUseful:
docker compose up api # just the API and its dependencies
docker compose logs -f api # follow one service
docker compose build --no-cache # after changing a Dockerfile or lockfile
docker compose down -v # stop and wipe the database volumeEach app's Dockerfile also carries a production target (compose uses dev),
which builds the app and runs it without the source mount.
Before deploying to beta or production, ensure all required environment variables are properly configured. This section details all secrets, credentials, and their sources.
.env files are gitignored by default.
./scripts/generate-secrets.sh to generate cryptographically secure random values for JWT and encryption keys.
These variables are required for the API to start:
| Variable | Description | Source | Example |
|---|---|---|---|
DATABASE_URL |
PostgreSQL connection string | Create a database and get the connection URL | postgresql://tavvio:password@localhost:5434/tavvio |
REDIS_URL |
Redis connection string | Redis instance (local or cloud) | redis://localhost:6379 |
NODE_ENV |
Application environment | Set to development, staging, or production |
development |
PORT |
API listen port | Choose any available port | 3000 |
Setup for local development:
docker compose up -d
# Database and Redis are now availableGenerate these using the provided script:
./scripts/generate-secrets.sh > secrets.txt
# Copy values from secrets.txt to .env| Variable | Description | Length | Source |
|---|---|---|---|
JWT_SECRET |
JWT signing key (HS256) | 64+ bytes (base64) | Run ./scripts/generate-secrets.sh |
BANK_SESSION_ENCRYPTION_KEY |
AES-256 key for sensitive data | 32 bytes (base64) | Run ./scripts/generate-secrets.sh |
BANK_WEBHOOK_SECRET |
Verify bank webhook integrity | 32+ bytes (base64) | Run ./scripts/generate-secrets.sh |
JWT_EXPIRY |
JWT token expiration | N/A | Set to 7d or preferred duration |
Example setup:
# Generate secrets
./scripts/generate-secrets.sh > .env.secrets
# Add to .env
cat .env.secrets >> .env| Variable | Description | Source | Format |
|---|---|---|---|
STELLAR_NETWORK |
Stellar environment | Set to testnet or mainnet |
testnet |
STELLAR_HORIZON_URL |
Stellar HTTP API endpoint | Use public endpoints or self-hosted | https://horizon-testnet.stellar.org |
STELLAR_SOROBAN_RPC_URL |
Soroban RPC endpoint | Use public endpoints or self-hosted | https://soroban-testnet.stellar.org |
STELLAR_RELAY_KEYPAIR_SECRET |
Relay account signing key | Generate via stellar key generate |
SXXXXXX... (starts with S) |
STELLAR_RELAY_PUBLIC_KEY |
Relay account public key | Derived from keypair | GXXXXXX... (starts with G) |
Generate Stellar keys:
# Install stellar CLI if needed
brew install stellar-cli
# Generate a new keypair
stellar key generate
# Output will show secret and public key
# IMPORTANT: Fund the account with testnet XLM at https://friendbot.stellar.orgDeploy contracts:
The Soroban contract IDs must be deployed and set in the environment:
| Variable | Description | Source |
|---|---|---|
SOROBAN_HTLC_CONTRACT_ID |
HTLC contract | Deploy contract/soroban and record the ID |
SOROBAN_SETTLEMENT_CONTRACT_ID |
Settlement contract | Deploy contract/soroban and record the ID |
SOROBAN_FEE_COLLECTOR_CONTRACT_ID |
Fee collector contract | Deploy contract/soroban and record the ID |
SOROBAN_ESCROW_CONTRACT_ID |
Escrow contract | Deploy contract/soroban and record the ID |
| Variable | Description | Source | Format |
|---|---|---|---|
EVM_RELAY_PRIVATE_KEY |
EVM relay account private key | Generate via MetaMask or ethers.js | 0xXXXX... (0x-prefixed hex, 64 chars) |
RPC_ETHEREUM |
Ethereum RPC endpoint | Alchemy, Infura, or public endpoint | https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY |
RPC_BASE |
Base RPC endpoint | Alchemy, Infura, or public endpoint | https://base-mainnet.g.alchemy.com/v2/YOUR_KEY |
RPC_BNB |
BNB Chain RPC endpoint | Binance public RPC | https://bsc-dataseed.binance.org |
RPC_POLYGON |
Polygon RPC endpoint | Polygon public RPC | https://polygon-rpc.com |
RPC_ARBITRUM |
Arbitrum RPC endpoint | Arbitrum public RPC | https://arb1.arbitrum.io/rpc |
RPC_AVALANCHE |
Avalanche C-Chain RPC endpoint | Avalanche public RPC | https://api.avax.network/ext/bc/C/rpc |
Deploy HTLC contracts on each chain:
| Variable | Description | Source |
|---|---|---|
HTLC_ADDRESS_ETHEREUM |
HTLC contract on Ethereum | Deploy contract/evm via Hardhat, record address |
HTLC_ADDRESS_BASE |
HTLC contract on Base | Deploy contract/evm via Hardhat, record address |
HTLC_ADDRESS_BNB |
HTLC contract on BNB Chain | Deploy contract/evm via Hardhat, record address |
HTLC_ADDRESS_POLYGON |
HTLC contract on Polygon | Deploy contract/evm via Hardhat, record address |
HTLC_ADDRESS_ARBITRUM |
HTLC contract on Arbitrum | Deploy contract/evm via Hardhat, record address |
HTLC_ADDRESS_AVALANCHE |
HTLC contract on Avalanche | Deploy contract/evm via Hardhat, record address |
Generate an EVM private key:
# Via ethers.js
node -e "const ethers = require('ethers'); const wallet = ethers.Wallet.createRandom(); console.log('Private Key:', wallet.privateKey); console.log('Address:', wallet.address);"
# Or import MetaMask: Export → Private Key (⚠️ keep secure)| Variable | Description | Source | When Required |
|---|---|---|---|
CIRCLE_API_KEY |
Circle CCTP API key | Circle Developer Portal | For stablecoin bridging |
WORMHOLE_ENV |
Wormhole environment | Set to Testnet or Mainnet |
For cross-chain messaging |
LAYERSWAP_API_KEY |
Layerswap API key | Layerswap Dashboard | For Starknet routing |
| Variable | Description | Source | Format |
|---|---|---|---|
STRIPE_SECRET_KEY |
Stripe secret API key | Stripe Dashboard | sk_test_... (test) or sk_live_... (production) |
STRIPE_WEBHOOK_SECRET |
Stripe webhook signing secret | Stripe → Webhooks → Signing secret | whsec_... |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY |
Stripe publishable key (public) | Stripe Dashboard | pk_test_... (test) or pk_live_... (production) |
Setup:
- Create a Stripe account
- Get API keys from Dashboard → API Keys
- Create a webhook endpoint pointing to
https://api.yourdomain.com/v1/webhooks/stripe - Copy the webhook signing secret
| Variable | Description | Source |
|---|---|---|
RESEND_API_KEY |
Resend email service API key | Resend Dashboard |
EMAIL_FROM |
From address for notifications | Your verified sender address |
| Variable | Description | Source |
|---|---|---|
CLOUDINARY_CLOUD_NAME |
Cloudinary cloud name | Cloudinary Dashboard |
CLOUDINARY_API_KEY |
Cloudinary API key | Cloudinary Settings → API Keys |
CLOUDINARY_API_SECRET |
Cloudinary API secret | Cloudinary Settings → API Keys (keep secure) |
| Variable | Description | Example |
|---|---|---|
API_URL |
Public API URL | https://api.yourdomain.com |
NEXT_PUBLIC_API_URL |
Frontend API endpoint | https://api.yourdomain.com or http://localhost:3000 |
WWW_URL |
Main website URL | https://useroutr.com or http://localhost:3000 |
DASHBOARD_URL |
Dashboard URL | https://dashboard.useroutr.com or http://localhost:3001 |
CHECKOUT_URL |
Checkout widget URL | https://checkout.useroutr.com or http://localhost:3002 |
USEROUTR_FEE_BPS |
Platform fee in basis points | 50 (0.5%) |
| Variable | Description | Source | Environment |
|---|---|---|---|
MONEYGRAM_HOME_DOMAIN |
MoneyGram federation domain | MoneyGram Developer Portal | Testnet: extstellar.moneygram.com / Mainnet: stellar.moneygram.com |
Minimum required for npm run start:dev:
DATABASE_URL=postgresql://tavvio:password@localhost:5434/tavvio
REDIS_URL=redis://localhost:6379
JWT_SECRET=<run: ./scripts/generate-secrets.sh>
NODE_ENV=development
PORT=3000
# STELLAR_RELAY_KEYPAIR_SECRET and EVM_RELAY_PRIVATE_KEY are only required
# when NODE_ENV=production; payment signing features need them at runtime.Add all bridge and payment service integrations:
- All Core Infrastructure variables
- All Authentication variables
- All Stellar variables (with testnet endpoints)
- All EVM variables (with testnet RPCs)
- Circle, Wormhole, Layerswap credentials
- Stripe test keys
- Resend API key
- All URLs pointing to staging domain
- Smart contracts have been audited
- All private keys are stored in a secure key management system (AWS Secrets Manager, HashiCorp Vault, etc.)
- Rate limiting and DDoS protection are configured
- Monitoring and alerting are in place
- Disaster recovery plan is documented
Production variables:
- All Core Infrastructure variables (using production database and Redis)
- All Authentication variables (regenerated and stored securely)
- All Stellar variables (mainnet endpoints)
- All EVM variables (mainnet RPCs, deployed contracts)
- All third-party service keys (production credentials)
- Production URLs and domains
The API automatically validates critical environment variables on startup. If any required variable is missing or contains a placeholder value, the API will fail to start with a clear error message.
Example error:
❌ Environment Configuration Errors:
STARTUP FAILED: STELLAR_RELAY_KEYPAIR_SECRET is a placeholder or missing. Set a valid value.
STARTUP FAILED: JWT_SECRET is a placeholder or missing. Set a valid value.
📋 To generate secrets, run: ./scripts/generate-secrets.sh
Issue: API fails to start with "JWT_SECRET is a placeholder"
Solution:
./scripts/generate-secrets.sh > .env.secrets
# Copy values to .env, ensure no placeholder patterns like "your-secret" remainIssue: "STELLAR_RELAY_KEYPAIR_SECRET not set"
Solution:
stellar key generate
# Fund the account: https://friendbot.stellar.org/?addr=GXXXXXX
# Add secret to .envIssue: "DATABASE_URL connection failed"
Solution:
docker compose up -d # Ensure Postgres is running
docker compose logs postgres # Check for errorsFrom apps/api:
npm run test
npm run test:e2e
npm run buildSoroban contracts:
cd contract/soroban
cargo testEVM contracts:
cd contract/evm
npm install
npx hardhat test- Production deployment requires contract audits, strict key management, and chain-specific bridge credentials.