Transfero OTC System Guide
Complete reference for administrators. Covers system architecture, trading flows, WhatsApp bot operation, security model, and day-to-day operations.
System Architecture
The OTC API is a single Fastify server that handles all trading operations: API clients (web dashboard), WhatsApp bot, and admin management. Everything runs in one process.
Clients
Web Dashboard
index.html
Admin Panel
admin.html
API Clients
Custom integrations
via Evolution API
Fastify Server
:8080REST API
Auth, Sessions, Closings, Prices
WebSocket
Live price stream (1s ticks)
Bot Module
WhatsApp command handler
Pricing Service
Spot rate + spread calc
Admin CRUD
Clients, Spreads, Controls
Audit Logger
Every mutation tracked
Infrastructure
PostgreSQL
Closings, clients, spreads, holidays, audit logs, controls
Redis
Sessions, config cache, bot state, message dedup
External APIs
ValorPro
USD/BRL spot rate
Evolution API
WhatsApp messaging
Telegram
Admin alerts
Single Process
All modules run in one Node.js process. No microservices, no message queues. Shared Redis and PostgreSQL connections.
Two Interfaces
REST API for web clients + webhook receiver for WhatsApp. Both use the same pricing, closing, and audit services.
Real-time Prices
WebSocket endpoint pushes price ticks every 1s. Spot rate fetched from ValorPro, spread applied per client tier.
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| Runtime | Node.js 20+, TypeScript 5.7 | Strict mode, ESM modules |
| Framework | Fastify 5 | HTTP server, WebSocket, route plugins |
| Validation | Zod | Request/response schema validation |
| Database | PostgreSQL 16, Prisma 6 | 6 tables: closings, audit_logs, clients, spreads, system_controls, holidays |
| Cache | Redis 7 (ioredis) | Sessions, config cache, bot state, message dedup |
| Auth | @fastify/jwt (HS256) | JWT tokens with role claims (client/admin) |
| Real-time | @fastify/websocket | Live price stream (1s ticks) |
| Logging | Pino | Structured JSON logs, no PII |
| Testing | Vitest | 179 tests across 13 suites |
Trade Flow (API)
All trades follow a quote-lock-confirm pattern. The client requests a price, the system locks it for 7 seconds, and the client confirms to execute.
/v1/sessions
Client sends currency (USDT/USDC), settlement (D0/D1/D2), and amount. System fetches spot rate, applies tier spread, creates a Redis session with 7s TTL. Returns locked price.
Client reviews the locked price and total BRL. They must confirm within 7 seconds or the quote expires automatically. No charge on expiry.
/v1/sessions/:id/close
Client confirms. Session is atomically marked as closed in Redis, closing is persisted to PostgreSQL, OID dedup key is set, audit log is written. Returns closing confirmation.
Client-facing privacy
The API never exposes spread percentages, tier information, or the internal side ("SELL") to clients. Clients always see side: "BUY" (their perspective). Spread and tier are only visible in admin endpoints.
Pricing & Spreads
Client price = spot rate x (1 + spread%/100). The spot rate comes from ValorPro (USD/BRL). Spread is determined by the client's tier, currency, and settlement window.
Tiers (T1-T7)
Each client is assigned a tier that determines their spread. Lower tier = lower spread = better price.
Settlement Windows
Settlement affects the spread. Faster settlement costs more (higher spread).
Example (T1 / USDT / spot = 5.00)
Managing Spreads
Spreads are configured in the Admin Panel → Spreads tab. There are 42 entries total (7 tiers x 2 currencies x 3 settlements).
To change a client's pricing: Edit their tier in Clients tab, or adjust the spread percentage in the Spreads tab.
Global adjustment: The spread_adjustment system control adds/subtracts from all spreads globally. Useful for temporary market conditions.
USDC/USDT ratio: The usdc_usdt_ratio control adjusts the USDC price relative to USDT.
Market Hours
Trading Hours
09:05 - 16:55
BRT (UTC-3), aligned with B3
Closed
Weekends, BR holidays, US holidays. Configure holidays in the Admin Panel → Holidays tab.
Outside Hours
Price grid is still accessible but quoting/closing is blocked. Bot responds with next opening time.
WhatsApp Bot Architecture
The bot does not connect to WhatsApp directly. It uses Evolution API, an open-source WhatsApp Web bridge that handles the WhatsApp connection and forwards messages as HTTP webhooks.
WhatsApp User Evolution API OTC API Server
────────────── ───────────── ──────────────
Sends message ──────────> Receives via
in group chat WhatsApp Web ──────────> POST /webhook/evolution
connection (always returns 200)
│
BotService processes
command (/ref, /off,
/fecha, /help, /pix)
│
Receives HTTP <────────── EvolutionClient.sendText()
POST request POST /message/sendText/{instance}
│
Receives bot <────────── Forwards to
reply in chat WhatsApp Web
Evolution API
Self-hosted service that maintains a WhatsApp Web session. Configured via:
Bot Module
Conditional on BOT_ENABLED=true. When disabled, no bot routes are registered. Zero impact on the API.
Bot Session Flow
Each bot trading session follows a strict state machine. Only one session per group at a time.
Price Selection
On /fecha, the trade price is min(lastQuote, secondLastQuote) -- always the best price for the client.
Client /off
If the client sends /off during QUOTING, quoting stops immediately and "Off" is sent. But /fecha is blocked after a client-initiated /off (only auto-Off allows closing).
Test Mode
Groups with no matching Client record use tier T7 and get "(TESTE)" prefixed messages. No trades are persisted to the database.
Message Dedup
Same message ID within 2 minutes is ignored. Prevents duplicate processing from Evolution API retries.
Bot Commands
| Command | Description | When |
|---|---|---|
/ref [vol] [currency] [settlement] |
Start quoting session | Anytime (market open) |
/off |
Stop quoting early | During QUOTING |
/fecha [vol] |
Confirm and execute trade | During CLOSING_WINDOW (auto-Off only) |
/help |
Show available commands | Anytime |
/pix |
Show PIX payment info | Anytime |
Typo Tolerance
The /fecha command accepts 38 aliases including common typos:
/fech, /fechar, /feha, /fechr, /fcha, /trava, /travar, /done, /close, /fechar, /fecah, /fechaa, ...
Volume can be in Brazilian format: 10k = 10,000, 1.5kk = 1,500,000, 200.400 = 200,400
Adding a Client to WhatsApp Bot
Create the WhatsApp group
Create a WhatsApp group with the client's phone number and the bot's WhatsApp number. Note the group ID from Evolution API (format: [email protected]).
Create or update the client
In the Admin Panel → Clients, create a new client (or edit existing). Set the WhatsApp Group ID field to the group ID obtained above. Also set Counterparty ID if this client will use the Notify Desk feature — it's the client's UUID in the BFF system, sent as the counterparty field when notifying the desk.
Verify the mapping
Send /ref 10000 USDT D0 in the group. The bot should respond with quotes and show the client's name (not "(TESTE)"). If you see "(TESTE)", the group ID mapping is incorrect.
Configure Evolution API webhook
Ensure Evolution API is configured to POST events to http://<server>:8080/webhook/evolution with event type messages.upsert.
Authentication
API Key Flow
1. Admin creates a client with a raw API key
2. Key is SHA-256 hashed and stored in DB
3. Client calls POST /v1/auth/login with the raw key
4. Server hashes the key, looks up the client, returns a JWT
5. Client includes JWT in Authorization: Bearer header
JWT Claims
Token algorithm: HS256. Expiry: 12 hours.
Raw API keys cannot be recovered
The database only stores the SHA-256 hash. If a client loses their key, you must rotate it via Admin Panel → Clients → Rotate Key. The old key stops working immediately.
Security Rules
Input validation
Every endpoint validates with Zod .strict(). Unknown fields are rejected.
Authorization
All routes except health checks and login require JWT. Admin routes check role=admin.
No PII in logs
API keys are never logged, only hashes. Pino redaction strips sensitive fields.
No secrets in code
All secrets via environment variables. JWT secret, DB URL, Redis URL, API keys.
RFC 7807 error responses
All errors return structured JSON with type, title, status, code, and traceId. Never exposes stack traces.
Idempotency (OID dedup)
Trade closings use an OID (Operation ID) as an idempotency key. Redis + DB fallback prevents double execution.
Client data privacy
Spread %, tier, and internal side ("SELL") are never exposed to client-facing APIs. Clients always see "BUY".
Complete audit trail
Every closing, admin action, and bot trade writes to the audit_logs table with IP address and timestamp.
Audit Trail
Every state-changing operation is recorded in the audit_logs table. View the full audit log in the Admin Panel → Operations tab.
| Action | Source | Description |
|---|---|---|
| SESSION_CREATED | API / Bot | Client requested a quote |
| CLOSE_SUCCESS | API / Bot | Trade confirmed and persisted |
| CLOSE_FAILED | API / Bot | Close attempt failed (expired/already closed) |
| client.create | Admin | New client created |
| client.update | Admin | Client properties changed |
| client.deactivate | Admin | Client deactivated (loses access) |
| client.rotate_key | Admin | API key rotated |
| spread.update | Admin | Spread configuration changed |
| control.update | Admin | System control toggled |
| BOT_REF | Bot | Bot quote session started |
| BOT_CLOSE | Bot | Bot trade executed |
Database
| Table | Purpose | Key Fields |
|---|---|---|
| closings | Confirmed OTC trades | oid, client_name, tier, currency, settlement, amount, price, total_brl, status (pending→funded→paid→settled) |
| audit_logs | All mutations | action, session_id, oid, ip_address, detail (JSON) |
| clients | API key hashes, tiers | name, api_key_hash, tier, role, group_id, counterparty_id, active |
| spreads | Per-tier spread config | tier, currency, settlement, spread_pct |
| system_controls | Global toggles | key (enum), value (string) |
| holidays | Market holidays | date, label, market (BR/US) |
PostgreSQL
pnpm db:migrateRedis
Common Admin Tasks
Onboard a new API client
1. Go to Admin → Clients → Add Client
2. Enter the client name, generate a strong API key, select their tier (T1-T7), and set role to "client"
3. Save the raw API key securely and share it with the client -- it cannot be recovered later
4. The client uses the key to call POST /v1/auth/login and receives a JWT
Adjust pricing for a specific client
Option A (change tier): Edit the client in Clients tab, change their tier. They immediately get the new tier's spreads.
Option B (custom spreads): Edit specific spread entries in Spreads tab. E.g., reduce T3/USDT/D0 from 1.00% to 0.80%.
Note: Spread changes take effect on the next price fetch. Active sessions keep their locked price.
Temporarily disable all trading
Go to Admin → Controls:
quoting_enabled = OFF -- Disables API quoting. Clients get a 503 error on price requests.
bot_enabled = OFF -- Disables the WhatsApp bot. Webhook still returns 200 but commands are ignored.
api_enabled = OFF -- Disables the entire public API. Only health checks and admin routes remain.
Add a market holiday
Go to Admin → Holidays. Enter the date, optional label, and market (BR or US).
Trading is blocked on days when either BR or US market is closed (both must be open).
Update the holiday calendar at the start of each year with the official BR and US calendars.
Revoke a compromised API key
Immediate: Deactivate the client in Clients tab. This blocks all authentication instantly.
Then: Rotate the key with a new one via the "Rotate Key" button. Re-activate the client.
Note: Existing JWTs will continue working until they expire (12h). For immediate revocation, deactivation is the fastest path.
Manage a trade through its lifecycle
Each closing follows the lifecycle: pending → funded → paid → settled (or cancelled from any step).
pending — Trade price is locked. Awaiting token acquisition.
funded — Tokens have been sourced from the fund and are ready to deliver.
paid — Client has transferred BRL to our account. Confirmed by the desk.
settled — Tokens have been delivered. Trade is complete.
To advance or revert: go to Admin → Operations, click any trade row to open its detail, then click the appropriate action button. Mistakes can be undone with the ↩ Revert button (available for funded and paid states).
After advancing to paid, use the Send to Desk button in the closing detail to notify the BFF system. The flow is derived automatically from the trade's settlement (D0→D0D0, D1→D1D1, D2→D2D2).
Deployment
Production Environment
Deploy Steps
1. Push to remote -- Commit and push changes to git
2. Rsync to server -- rsync -avz --exclude node_modules --exclude .env --exclude .git ./ [email protected]:/home/ubuntu/otc-api/
3. SSH into server -- ssh -i otc-api.pem [email protected]
4. Install & migrate -- cd /home/ubuntu/otc-api && pnpm install && npx prisma generate && npx prisma migrate deploy
5. Restart -- pm2 restart otc-api
6. Verify -- Check /health/ready returns 200
Troubleshooting
"SPOT_UNAVAILABLE" errors
The ValorPro API is not responding. Check if the ValorPro service is accessible from the server. This blocks all pricing and trading.
/health/ready returns 500
Either PostgreSQL or Redis is unreachable. Check connection strings in .env. Run pm2 logs otc-api to see the specific error.
Bot not responding to WhatsApp messages
Check: (1) Is BOT_ENABLED=true? (2) Is the bot_enabled system control ON? (3) Is Evolution API running and configured to POST to the webhook URL? (4) Check pm2 logs for errors.
Bot shows "(TESTE)" for a real client
The WhatsApp group ID is not mapped to a client. Go to Admin → Clients, edit the client, and set the correct group_id (format: [email protected]).
Client can't log in after key rotation
Make sure you gave the client the new raw key (not the hash). The hash is displayed in the admin panel for reference only. Also verify the client is still active.
Transfero -- Crypto Financial Infrastructure for Latin America