GET /v1/admin/wallet-alerts lists wallet receives that match no deposit
payment — coins in custody that no user has been credited for. It filters on
WalletTxoAlert.status == "open"
(src/crypto_processing_api/api/admin.py:147).
Nothing in the codebase ever sets that status to anything else, and nothing
ever sets resolved_at. The column exists
(src/crypto_processing_api/ledger/models.py:498); the only writer is the
insert in src/crypto_processing_api/workers/reconciliation.py:287.
So an operator who does the work — reads
docs/runbook-usdt-attribution.md,
finds the user, credits them through the review queue — has no way to clear the
alert. The queue only grows, and a queue that only grows stops being read,
which is exactly how the next real one gets missed.
What to build
POST /v1/admin/wallet-alerts/{id}/resolve, taking a short note, setting
status and resolved_at, and recording the acting key id.
Constraints
- This must move no money. It records that a human looked. Crediting a user
is a separate action that already exists
(POST /v1/admin/deposits/{id}/resolve), and the two must not be fused — an
operator clearing an alert must not be able to mint a balance by accident.
- Reuse the shape of the existing admin write endpoints in
src/crypto_processing_api/api/admin.py: admin scope, key.key_id as the
actor, 409 if it is already resolved.
- The route needs a response model and an
operation_id like every other route
(src/crypto_processing_api/api/schemas.py). Adding one means running
python scripts/export_openapi.py and committing the result, or CI fails.
- Add the new response to the byte corpus in
tests/integration/test_wire_bytes.py — regenerate it with
WIRE_GOLDEN_WRITE=1. A route with no pinned bytes fails
test_every_route_has_an_operation_id_and_pinned_bytes.
Worth deciding
Whether a resolved alert stays listable (?status=resolved). An audit trail an
operator cannot read back is not much of one.
GET /v1/admin/wallet-alertslists wallet receives that match no depositpayment — coins in custody that no user has been credited for. It filters on
WalletTxoAlert.status == "open"(
src/crypto_processing_api/api/admin.py:147).Nothing in the codebase ever sets that status to anything else, and nothing
ever sets
resolved_at. The column exists(
src/crypto_processing_api/ledger/models.py:498); the only writer is theinsert in
src/crypto_processing_api/workers/reconciliation.py:287.So an operator who does the work — reads
docs/runbook-usdt-attribution.md,finds the user, credits them through the review queue — has no way to clear the
alert. The queue only grows, and a queue that only grows stops being read,
which is exactly how the next real one gets missed.
What to build
POST /v1/admin/wallet-alerts/{id}/resolve, taking a short note, settingstatusandresolved_at, and recording the acting key id.Constraints
is a separate action that already exists
(
POST /v1/admin/deposits/{id}/resolve), and the two must not be fused — anoperator clearing an alert must not be able to mint a balance by accident.
src/crypto_processing_api/api/admin.py:adminscope,key.key_idas theactor,
409if it is already resolved.operation_idlike every other route(
src/crypto_processing_api/api/schemas.py). Adding one means runningpython scripts/export_openapi.pyand committing the result, or CI fails.tests/integration/test_wire_bytes.py— regenerate it withWIRE_GOLDEN_WRITE=1. A route with no pinned bytes failstest_every_route_has_an_operation_id_and_pinned_bytes.Worth deciding
Whether a resolved alert stays listable (
?status=resolved). An audit trail anoperator cannot read back is not much of one.