Transfero

System Guide

Transfero OTC — Admin Reference

Admin API Docs

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

WhatsApp

via Evolution API

REST + WS + Webhooks

Fastify Server

:8080

REST 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
RuntimeNode.js 20+, TypeScript 5.7Strict mode, ESM modules
FrameworkFastify 5HTTP server, WebSocket, route plugins
ValidationZodRequest/response schema validation
DatabasePostgreSQL 16, Prisma 66 tables: closings, audit_logs, clients, spreads, system_controls, holidays
CacheRedis 7 (ioredis)Sessions, config cache, bot state, message dedup
Auth@fastify/jwt (HS256)JWT tokens with role claims (client/admin)
Real-time@fastify/websocketLive price stream (1s ticks)
LoggingPinoStructured JSON logs, no PII
TestingVitest179 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.

1
POST /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.

|
2
7s Confirmation Window

Client reviews the locked price and total BRL. They must confirm within 7 seconds or the quote expires automatically. No charge on expiry.

|
3
POST /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.

T1Institutional (lowest spread)
T2-T3Premium clients
T4-T5Standard clients
T6Retail / small volume
T7Test / default (highest spread)

Settlement Windows

Settlement affects the spread. Faster settlement costs more (higher spread).

D0Same-day settlement (highest spread)
D1Next business day
D2Two business days (lowest spread)

Example (T1 / USDT / spot = 5.00)

D0: 5.00 x 1.003 = 5.0150
D1: 5.00 x 1.0015 = 5.0075
D2: 5.00 x 1.0005 = 5.0025

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:

EVOLUTION_API_URL = server address
EVOLUTION_API_KEY = API authentication
EVOLUTION_INSTANCE_NAME = WhatsApp instance

Bot Module

Conditional on BOT_ENABLED=true. When disabled, no bot routes are registered. Zero impact on the API.

BOT_ENABLED = true/false
QUOTE_INTERVAL_SECONDS = 5
MAX_QUOTES_PER_SESSION = 7

Bot Session Flow

Each bot trading session follows a strict state machine. Only one session per group at a time.

/ref QUOTING 7 quotes at 5s intervals
After 7th quote: 5s delay
"Off" CLOSING_WINDOW 5s to send /fecha
/fecha or timeout
/fecha TERMINATED Trade persisted to DB

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

1

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]).

2

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.

3

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.

4

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

apiKeyHashSHA-256 hash (client identifier)
clientNameDisplay name
tierT1-T7 (pricing tier)
role"client" or "admin"
groupIdWhatsApp group (nullable)

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

1

Input validation

Every endpoint validates with Zod .strict(). Unknown fields are rejected.

2

Authorization

All routes except health checks and login require JWT. Admin routes check role=admin.

3

No PII in logs

API keys are never logged, only hashes. Pino redaction strips sensitive fields.

4

No secrets in code

All secrets via environment variables. JWT secret, DB URL, Redis URL, API keys.

5

RFC 7807 error responses

All errors return structured JSON with type, title, status, code, and traceId. Never exposes stack traces.

6

Idempotency (OID dedup)

Trade closings use an OID (Operation ID) as an idempotency key. Redis + DB fallback prevents double execution.

7

Client data privacy

Spread %, tier, and internal side ("SELL") are never exposed to client-facing APIs. Clients always see "BUY".

8

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_CREATEDAPI / BotClient requested a quote
CLOSE_SUCCESSAPI / BotTrade confirmed and persisted
CLOSE_FAILEDAPI / BotClose attempt failed (expired/already closed)
client.createAdminNew client created
client.updateAdminClient properties changed
client.deactivateAdminClient deactivated (loses access)
client.rotate_keyAdminAPI key rotated
spread.updateAdminSpread configuration changed
control.updateAdminSystem control toggled
BOT_REFBotBot quote session started
BOT_CLOSEBotBot trade executed

Database

Table Purpose Key Fields
closingsConfirmed OTC tradesoid, client_name, tier, currency, settlement, amount, price, total_brl, status (pending→funded→paid→settled)
audit_logsAll mutationsaction, session_id, oid, ip_address, detail (JSON)
clientsAPI key hashes, tiersname, api_key_hash, tier, role, group_id, counterparty_id, active
spreadsPer-tier spread configtier, currency, settlement, spread_pct
system_controlsGlobal toggleskey (enum), value (string)
holidaysMarket holidaysdate, label, market (BR/US)

PostgreSQL

Port: 5435 (dev) / 5432 (prod)
ORM: Prisma 6
Migrations: pnpm db:migrate

Redis

Port: 6381 (dev) / 6379 (prod)
Session TTL: 7s (API) / 5min (bot)
Dedup TTL: 120s (bot messages)

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

Server
Process manager
pm2 (name: otc-api)
App port
8080 (behind nginx :80)
PostgreSQL
localhost:5432
Redis
localhost:6379

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 Transfero OTC

Transfero -- Crypto Financial Infrastructure for Latin America