Recon Service
Status
Active
Date
2026-04-28
Owners
- Platform Backend
Last Verified Commit
56362a7a
Runtime
API + standalone worker
Purpose
recon_service owns SMS automation and reconciliation behavior migrated from the
legacy middle_server shooter and Pushbullet block.
Primary Entry Points
Internal owner routes:
/internal/recon/pushbullet/*/internal/recon/shooter/*/internal/recon/stats/*
Those internal routes are protected by the per-caller internal auth contract:
callers send X-Caller-Service plus the matching
INTERNAL_SERVICE_TOKEN_<CALLER> token. The legacy shared
INTERNAL_SERVICE_TOKEN is a Stage A/B fallback only and is not intended for
direct browser or frontend use.
External compatibility for bo/admin is served through admin_service, not by
calling recon_service directly.
Dependencies
- PostgreSQL
- Redis
wallet_serviceadmin_service- internal service token
- optional OpenRouter config
- optional Telegram bot token
Background Work
recon_worker runs the domain loops for:
- Pushbullet listener supervision
- missing SMS sync
- SMS ID sync
- phone whitelist validation
- template parsing
- order matching
- Telegram polling
Owned Data
shooter_pushbulletshooter_smsshooter_phoneshooter_template_rechargeshooter_device
Events
Emits:
- no Redis stream contract is currently documented for recon
Consumes:
- no Redis stream contract is currently required
Cross-service coordination currently happens through internal HTTP calls to:
wallet_serviceadmin_service
Health
- API
/healthchecks DB and Redis recon_workerreadiness depends on freshness-based checks for every critical loop
Key Env Vars
DATABASE_URLREDIS_URLWALLET_SERVICE_URLADMIN_SERVICE_URLMULTI_BRAND_ENFORCEMENT— required; one ofoff/observe/enforce. Production target isenforcepost-Phase-16. Drivesmulti_brand_enforcement_mode{service="recon_service"}gauge and the cross-brand reject decision inbrand_check.py(recon_cross_brand_rejected_total).BRAND_SIGNING_KEY— required in production; HMAC-SHA256 secret used to signX-Brand-Signatureon outbound brand-scoped wallet writes from recon-driven approvals.INTERNAL_SERVICE_TOKEN_RECON— required in production; per-caller token presented to wallet when recon calls it.PER_CALLER_TOKEN_REQUIRED—onis the Phase 16 target; activates the legacy-token hard-reject (T4-D-I2) on inbound/internal/recon/*calls.INTERNAL_SERVICE_TOKEN— legacy single-shared-token; deprecated. Phase 16 release gate requires the bare variant to be absent.OPENROUTER_API_KEYOPENROUTER_MODELTELEGRAM_BOT_TOKEN
Compatibility Notes
The previous back-office compatibility surface
(admin_service/app/api/routes/legacy_recon.py) was removed by ADR-009
together with the rest of the admin_service staff-coupled legacy
files. Recon back-office routes that depended on legacy admin auth,
status/msg/data envelopes, and Authorization header refresh are no
longer served. Replacements are out of scope of ADR-009.
Multi-Brand Constraints
Per ADR-009:
- the
shooter_*table family is split: operator-infrastructure rows (shooter_device,shooter_pushbullet,shooter_phone,shooter_template_recharge) remain brand-global because they describe operator-owned hardware, API tokens, whitelists, and parsing rules that are not per-brand; onlyshooter_smsand any player- or deposit-referencing recon review-state rows carrybrand_id shooter_sms.brand_idpropagation contract:- On INSERT (Pushbullet listener / SMS-ID sync / Telegram bot ingests a raw SMS), the row is created with
brand_idNULL because the parser cannot derive brand from raw message text. Migration0037(T9-E) relaxed the column to nullable so the ingest path stopsIntegrityError-ing — the previous tightening from0024cwas too strict for tables that receive their brand correlation post-INSERT. - At match time (the order-matching loop pairs an inbound SMS with a
player_deposit),shooter_sms.brand_idis rewritten fromplayer_deposit.brand_id. The matched deposit is the authoritative brand source for the SMS row. The match-time UPDATE includes a fail-loud check that the column is non-NULL after the write — seerecon_store.py::confirm_sms_matchfor the assertion that surfaces a logic bug if the brand fails to land. - Rows that pre-date
player_depositcreation (legacy SMS captured before any deposit existed) carry thedefaultbrand from the0024bbackfill pass and are never re-stamped retroactively. A post-flip cross-brand match attempt against such a row (e.g. an SMS bound todefaultmatching abrand2deposit) incrementsrecon_cross_brand_rejected_total{command="sms_match",mode}and is hard-rejected inenforcemode; operators must reconcile or abandon those rows manually. shooter_sms.brand_idis never parsed from message text, even when the text contains a brand-identifying token. Brand attribution flows only through the matched deposit so a forged SMS cannot mis-route to a brand it should not touch.
- On INSERT (Pushbullet listener / SMS-ID sync / Telegram bot ingests a raw SMS), the row is created with
- approvals still flow through
wallet_service; the per-brand wallet validation behavior (observeproceeds with request brand,enforcerejects) is gated byMULTI_BRAND_ENFORCEMENTinwallet_service
Tests
cd servers_v2/recon_service && uv run pytest- key suites:
tests/test_recon_routes.pytests/test_worker_health.py