Backend API
Overview#
The backend API serves three critical roles:
- Hidden payload store: Stores target price, direction, and salt until they should be revealed
- Event indexer: Reads contract events and indexes them into PostgreSQL for fast queries
- Public API: Serves indexed data and delivers payloads to authenticated buyers
Built with FastAPI + uvicorn, PostgreSQL 16, SQLAlchemy 2, and Alembic for migrations.
API Routes#
Public Routes (No Auth)#
/signalsQuery params: status (active, resolved, cancelled), asset, maker, sort (newest, cheapest, most_buyers), limit, page.
/signals/{id}/makersQuery params: sort (win_rate, total_signals, volume), limit, page.
/makers/{address}/statsAuthenticated Routes (EIP-191 Signature)#
/signals/submitBody: { asset, target_price, direction, expiry_time, confidence? }. Returns commitment_hash. The backend generates a 32-byte salt and stores the full payload.
/signals/linkBody: { signal_id, commitment_hash }. Called after on-chain createSignal() confirms.
/signals/{id}/payloadReturns target price and direction. Only accessible to addresses with a purchase record on-chain. Returns 403 for unauthorized access — deliberately returns the same error whether the signal doesn't exist or the caller hasn't purchased (anti-enumeration).
Internal Routes (API Key)#
/internal/signals/pending/internal/signals/by-hash/{hash}/paramsReturns target price, direction, and salt for the keeper to submit resolveSignal().
Authentication#
Two authentication methods:
| Method | Used By | Header |
|---|---|---|
| EIP-191 Signature | Makers, Buyers | AGDEL-Signature: address:timestamp:signature |
| API Key | Keeper Bot | X-API-Key: <key> |
EIP-191 signatures have a ±5 minute timestamp tolerance. Authorization headers are capped at 2KB. API key comparison uses hmac.compare_digest for timing-safety.
Database Schema#
signals
signal_id (PK), maker, asset, expiry_time, cost_usdc,
commitment_hash, resolved, outcome, target_price,
direction, protocol_fee, entry_price, resolution_price,
quality_score, direction_score, precision_multiplier,
difficulty_weight, confidence, signal_name, signal_description
purchases
signal_id (FK), buyer, amount, purchased_at,
refunded, refund_amount
UNIQUE(signal_id, buyer)
makers
address (PK), total_signals, wins, losses,
active_signals, total_volume, win_rate,
avg_quality_score, brier_score, first_seen_at, last_active_at
signal_payloads
commitment_hash (UNIQUE), signal_id (FK after linking),
maker, asset, target_price, direction, salt
indexer_state
last_indexed_blockEvent Indexer#
The indexer polls eth_getLogs every 5 seconds and processes four event types:
- SignalCreated: Inserts signal record with public metadata
- SignalPurchased: Inserts purchase record, updates signal escrow total
- SignalResolved: Updates signal with outcome, revealed price/direction, computes quality score, updates maker stats
- RefundClaimed: Updates purchase refund status and amount
The target_price and direction columns in the signals table stay NULL until a SignalResolved event is indexed. This prevents information leaks before resolution.
Rate Limiting#
| Route Group | Limit |
|---|---|
| Public reads (/signals, /makers, /stats) | 60 requests/min |
| Payload delivery (/signals/{id}/payload) | 10 requests/min |
| Signal submission (/signals/submit, /link) | 20 requests/min |
| Internal (/internal/*) | No limit (API key auth) |
Price Format#
All monetary values are serialized as strings in API responses to prevent JavaScript floating-point precision loss. On-chain prices are uint256 scaled by 1e8. USDC amounts use 6 decimal places.