The screening endpoint accepts checksum-validated classic (G...) and muxed (M...) Stellar account IDs and forwards them unchanged to Horizon.
Read-only FastAPI service for screening Stellar accounts and reading Soroban contract events. It fetches account activity from Horizon, exposes network status and events from Stellar RPC, and does not hold signing keys or submit transactions. Screening scores are transparent heuristics, not proof of fraud or financial/compliance advice. The app reuses a bounded HTTP connection pool for the lifetime of the process and closes it during shutdown.
Screening, network-status, and event routes publish Pydantic response schemas and representative examples in /openapi.json. Optional upstream event and ledger fields remain nullable.
API responses include baseline browser security headers. Risk-score responses use Cache-Control: no-store because they contain account-specific screening results.
Every HTTP response includes X-Request-ID. A supplied ID is reused only when it is 1–64 ASCII letters, digits, dots, underscores, or hyphens; otherwise the service generates a UUID and uses the selected value for request correlation.
GET /health and GET /live are dependency-free liveness checks. GET /ready probes the configured Horizon fee-stats endpoint and Soroban RPC health method using REQUEST_TIMEOUT_SECONDS. It returns HTTP 200 when both are reachable, or HTTP 503 with a per-dependency ok/unavailable status when either is degraded. Upstream exception details are not included in the response.
flowchart LR
Browser[Next.js dashboard] -->|CORS HTTP| API[FastAPI app]
API -->|POST /risk/score| Score[Account screening]
Score -->|account and recent operations| Horizon[Stellar Horizon]
API -->|GET /events, /network/status| Reader[Soroban RPC reader]
Reader --> RPC[Stellar RPC]
Contract[Soroban Sentinel contract] -->|flagged events| RPC
The backend is an event reader, not a transaction writer. The example configuration points to the current Testnet deployment. Set CONTRACT_ID to a deployed contract on the selected network; without it, /events returns HTTP 503 rather than fabricated data. The service indexes new events into a local SQLite database while running, decodes both flagged and digest-bearing flaggedv2 events, and returns the stored history.
GET /accounts/{address}/operations?limit=20&cursor=... returns a page of normalized Horizon operations. The page contains the operation ID/type, creation time, transaction hash, participating accounts, and an amounts array. Each amount keeps its own asset type, code, and issuer; path payments may return separate source and destination amounts. next_cursor is an opaque token for the following page and is null when the current page is short. Page size is limited to 1–100. These values are descriptive activity data and do not alter the risk score.
Request logs are JSON records containing the method, route template, status, latency, and optional request ID. Raw paths, query strings, headers, request bodies, and upstream exception details are not logged; unmatched routes use a fixed label.
The configured contract is CCZAAZ3FJ7LKZA7E7A6EKQTU2HCNVI3YUVIHKWHSULGZSWAJFS2D2XVX, initialized with threshold 70. GET /events is connected to its Soroban RPC event stream and currently returns an empty event list; no monitoring agent has been authorized yet. See the contract repository's Testnet deployment runbook for transaction links and redeployment commands.
app/main.py— FastAPI app, CORS, and route registration.app/config.py— environment-backed settings.app/routers/— health, screening, and event endpoints.app/stellar.py— Horizon scoring data and Soroban RPC clients/event decoding.app/agents/pipeline.py— standalone deterministic scoring utility; the HTTP screening route uses live Horizon data throughapp/stellar.py.tests/test_api.py— mocked API tests; no external chain calls are required.
Requires Python 3.11 or newer.
python -m venv .venv
source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
pip install -r requirements.txt
cp .env.example .env
uvicorn app.main:app --reloadOpen http://localhost:8000/docs. Set the frontend's NEXT_PUBLIC_API_BASE_URL to this API origin and allow the frontend origin in CORS_ORIGINS.
For private deployments, set a random API_KEY of at least 32 characters and send it as Authorization: Bearer <API_KEY> to business API routes. /health, API documentation, and CORS preflight remain accessible. Leave API_KEY empty for local development.
| Command | Purpose |
|---|---|
uvicorn app.main:app --reload |
Run the development API server. |
python -m pytest |
Run tests with mocked Horizon and Soroban RPC responses. |
No Python linter is configured in the repository or CI yet. The CI workflow installs requirements.txt and runs python -m pytest.
Copy .env.example to .env; environment variables override file values. Use matching Horizon, Soroban RPC, and network passphrase values for one Stellar network.
| Variable | Default | Purpose |
|---|---|---|
HORIZON_URL |
https://horizon-testnet.stellar.org |
Account and operations data source. |
SOROBAN_RPC_URL |
https://soroban-testnet.stellar.org |
Network status and contract event source. |
NETWORK_PASSPHRASE |
Test SDF Network ; September 2015 |
Network identifier returned to the UI. |
CONTRACT_ID |
Stellar Sentinel Testnet contract | Deployed contract ID for /events; use a contract on the configured network. |
ENVIRONMENT |
development |
Runtime environment label. |
REQUEST_TIMEOUT_SECONDS |
8.0 |
Outbound HTTP timeout. |
UPSTREAM_MAX_RETRIES |
2 |
Number of retries for safe read requests after transient network or HTTP failures; maximum is 3. |
UPSTREAM_RETRY_BACKOFF_SECONDS |
0.2 |
Exponential retry backoff base in seconds; maximum is 1. |
UPSTREAM_RETRY_AFTER_CAP_SECONDS |
2.0 |
Maximum delay honored from Retry-After or computed backoff; maximum is 5 seconds. |
OPERATION_SCAN_LIMIT |
200 |
Maximum recent operations examined across Horizon pages; hard maximum is 1,000 and each page contains at most 200. |
ACTIVITY_WINDOW_DAYS |
7 |
Recent activity screening window. |
SCREENING_CACHE_TTL_SECONDS / SCREENING_CACHE_MAX_ENTRIES |
15 / 256 |
Successful screening response cache TTL and in-process entry limit. Set TTL to 0 to disable. |
RISK_POLICY_VERSION |
1.0.0 |
Semantic version returned with each screening result; bump when scoring semantics change. |
RISK_ACTIVITY_BURST_MIN_OPERATIONS / RISK_ACTIVITY_BURST_POINTS |
50 / 25 |
Operation-count signal cutoff and points. |
RISK_TRANSFER_VOLUME_XLM_THRESHOLD / RISK_TRANSFER_VOLUME_POINTS |
10000 / 25 |
Native XLM volume signal cutoff and points. |
RISK_COUNTERPARTY_MIN_COUNT / RISK_COUNTERPARTY_POINTS |
20 / 25 |
Distinct-counterparty signal cutoff and points. |
RISK_LOW_SEQUENCE_MAX / RISK_LOW_SEQUENCE_POINTS |
5 / 15 |
Low-sequence signal cutoff and points. |
RISK_HIGH_SCORE_THRESHOLD / RISK_ELEVATED_SCORE_THRESHOLD |
70 / 40 |
High and elevated risk-level boundaries; high must exceed elevated. |
EVENTS_LOOKBACK_LEDGERS |
50000 |
First-page event search window, clamped to RPC retention. |
CORS_ORIGINS |
http://localhost:3000 |
Comma-separated browser origins allowed to call the API. |
API_KEY |
empty | Optional bearer key for business API routes; use at least 32 random characters when enabled. |
SCREENING_RATE_LIMIT_REQUESTS |
30 |
Maximum account-screening requests per client in the configured window. |
SCREENING_RATE_LIMIT_WINDOW_SECONDS |
60 |
Sliding-window length for screening requests. |
TRUSTED_PROXY_CIDRS |
empty | Comma-separated IPs/CIDRs of reverse proxies allowed to supply X-Forwarded-For. |
EVENT_STORE_PATH |
./data/events.sqlite3 |
SQLite file used for indexed Soroban flag history and the resume cursor. |
EVENT_INGEST_INTERVAL_SECONDS |
30 |
Delay between event-indexing polls while the app is running. |
Only POST /risk/score is rate-limited; health, events, and network status remain available. A client that exceeds its quota receives HTTP 429 with Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers. The limiter uses an in-memory sliding window per application process, so deployments with multiple workers or replicas should enforce a shared limit at their gateway. Forwarded client addresses are used only when the direct peer matches TRUSTED_PROXY_CIDRS; configure the exact proxy ranges and ensure the proxy overwrites or appends X-Forwarded-For correctly. With no trusted ranges configured, the middleware uses the direct peer address and ignores forwarded headers.
Do not commit .env, account secrets, signing keys, or tokens. API_KEY is optional for local development and should be configured for private deployments.
Upstream retry behavior applies only to read-only Horizon requests and Soroban RPC calls. It retries transport errors and selected transient statuses, respects numeric or HTTP-date Retry-After values within the configured cap, and leaves client errors and JSON-RPC application errors untouched.
The SQLite database creates flag_events(scope, event_id, ledger, created_at, agent, subject, score_json, contract_id, tx_hash) and ingestion_state(scope, cursor) automatically. The (scope, event_id) primary key makes replay idempotent; scope is the configured network and contract. The service stores the RPC resume cursor and continues after restarts. Back up EVENT_STORE_PATH along with application config; deleting or restoring an older database makes ingestion resume from that database's cursor. Local indexed history begins with the RPC provider's current retained window and only preserves events observed after indexing starts; it cannot recover events already pruned upstream. If RPC is temporarily unavailable, /events serves indexed records and marks source.ingestion_status as stale. For multiple application replicas, use one ingestion worker and a supported shared SQLite volume; SQLite is not intended as a network database. Schema is initialized on startup; schema changes should be shipped with explicit migrations.
The score uses a bounded sample of recent Horizon operations across pages, up to the configured OPERATION_SCAN_LIMIT (maximum 1,000). The response's activity_sample reports the effective scan cap, actual records scanned, and whether the result may be incomplete because it reached the cap. Scoring uses configurable, validated signal thresholds and weights; the final score is capped at 100. Scores use observed activity rather than complete account history. This is not a trained model. RPC event history is provider-limited and is not a complete archive. Configure a persistent indexer for long-term event history.
Successful screening responses may be cached in-process for SCREENING_CACHE_TTL_SECONDS (default 15), keyed by network, Horizon URL, account, and active settings. The cache is bounded by SCREENING_CACHE_MAX_ENTRIES (default 256); set the TTL to 0 to disable it. Each application worker has its own cache.
Screening responses include scoring_policy_version. Set RISK_POLICY_VERSION to a semantic version and bump it when scoring signal meaning or scoring rules change; operational configuration should label any customized policy with its own version.
The /risk/score response includes an assets array with each Horizon balance and its asset identity, plus metrics.trustline_count. Issued-asset balances remain separate from the native XLM balance and do not affect the screening score. Malformed balance records are skipped.