ASA Sentinel API
One endpoint returns a control-risk report for an Algorand Standard Asset (ASA). Payment uses the x402 protocol: an HTTP 402 challenge, paid in USDC by the caller's own wallet.
POST /v1/reportsStart here
A report checks who controls an ASA — its manager, freeze, clawback and reserve rights — plus recent authority changes, rekey status on those control addresses, and how much of the holder set could actually be confirmed. It returns a risk level (LOW, MEDIUM, HIGH or UNKNOWN) with evidence for every finding.
Each deployment serves a single Algorand network. Don't assume mainnet — read network_name from GET /v1/config/public before integrating. The same endpoint returns the current USDC asset id and the exact price this deployment charges per report.
200 OK
{
"network": "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
"network_name": "testnet",
"algod_url": "https://testnet-api.algonode.cloud",
"usdc_asset_id": 10458941,
"price_usdc": "0.01"
}ASA Sentinel never holds your funds or your keys. You sign the payment yourself, from your own wallet — the server only ever receives a signed transaction (or a gateway-specific proof) to verify and settle.
Request flow
- Request, no proof
POST /v1/reportswith{"asset_id": <number>}and noX-Payment-Proofheader. - Read the challengeA
402response body carrieschallenge: price, USDC asset id, pay-to address, nonce and expiry. - Sign the paymentBuild and sign the payment with your own wallet — the challenge tells you what to sign.
- Retry with proofSame
POST /v1/reports, now withX-Payment-Proof: <signed proof JSON>. - Get a 202Normal outcome: a processing envelope with
payment_idandretrieval_token. Generation runs in the background. - Poll for the report
GET /v1/reports/:payment_id?token=<retrieval_token>, waitingretry_after_secondsbetween attempts, until it returns200or a terminal402.
A direct 200 from POST /v1/reports happens only when you resubmit a proof for a payment that has already completed — the same report is returned again rather than being regenerated.
API reference
POST /v1/reports
Request body:
| Field | Type | Notes |
|---|---|---|
| asset_id | integer | Required. The Algorand ASA id to check. |
Responses:
| Status | Meaning |
|---|---|
| 402 | No valid payment yet — body carries a payment challenge, or (with a proof) the gateway rejected the payment. |
| 202 | Payment accepted; report generation is running in the background. Body carries payment_id and retrieval_token. |
| 200 | Idempotent replay only — the proof matches an already-completed payment. Body is the report; X-Payment-Id and X-Retrieval-Token headers repeat the ids. |
| 400 | Missing/invalid asset_id, malformed X-Payment-Proof JSON, or an unknown/expired challenge nonce. |
| 403 | The submitted proof doesn't match the challenge it claims to answer. |
| 429 | Rate limited — see Failure and retry behavior. |
{
"error": "payment required",
"challenge": {
"priceUsdc": "0.01",
"usdcAssetId": 10458941,
"payTo": "<pay-to-address>",
"nonce": "<challenge-nonce>",
"expiresAt": "2026-07-24T12:05:00.000Z"
}
}{
"payment_id": "<uuid>",
"status": "report_queued",
"status_url": "/v1/payments/<uuid>",
"retrieval_token": "<retrieval-token>",
"retry_after_seconds": 3
}GET /v1/reports/:id
:id is the payment_id, not a separate report id — no report row exists until generation completes. Pass the retrieval token as ?token= or an X-Retrieval-Token header.
| Status | Meaning |
|---|---|
| 200 | Completed — full report body. |
| 202 | Still processing — same envelope shape as above. |
| 402 | Terminal failure — failure_code and a plain-language detail. |
| 401 | No token supplied. |
| 403 | Token doesn't match this payment. |
| 404 | Unknown payment id. |
GET /v1/payments/:id
Same authentication, a lighter status-only envelope — useful for polling without re-fetching the full report body: payment_id, status, report_id, failure_code, detail, retry_after_seconds.
Retrieval tokens
The retrieval token is signed and scoped to one payment and one payer wallet — it can't be reused for a different payment or forged for a different buyer. Treat it like a bearer credential for that one report: don't log it, and don't share a report link that contains it with anyone you don't want to see that report.
Report contents
Fields on the completed report object:
| Field | What it holds |
|---|---|
| riskLevel | LOW / MEDIUM / HIGH / UNKNOWN — see the methodology for how it's derived. |
| asset | Id, name, unit name, total supply, decimals, creator address, and the round the report was evaluated at. |
| controlRights | Active manager / freeze / clawback / reserve addresses (omitted if disabled or set to the zero address), defaultFrozen, and roleOverlaps. |
| recentConfigChanges | Configuration transactions found inside configChangeScanWindow (a concrete round range, not an unbounded "ever"). |
| rekeyStatus | isRekeyed, matched recentEvents, addresses confirmed rekeyed with no matching event in the scan window (rekeyedWithoutEvent), and control addresses that don't exist as funded accounts (addressesNotFound). |
| holderDistribution | holdersScanned, an optional creatorSharePct, and dataIncomplete — see Method and limits. |
| findings | Array of { ruleId, severity, explanation, evidence } — every finding names the rule that produced it and the on-chain evidence behind it. |
| rulesetVersion | Version string of the rule catalogue that produced this report. |
| generatedAt | ISO timestamp. |
| disclaimer | Plain-language reminder that this is technical evidence, not investment advice. |
{
"assetId": 10458941,
"riskLevel": "HIGH",
"controlRights": {
"clawback": { "address": "CLAWBACKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" },
"defaultFrozen": false,
"roleOverlaps": false
},
"holderDistribution": { "holdersScanned": 842, "creatorSharePct": 18.4, "dataIncomplete": false },
"findings": [
{
"ruleId": "unknown-clawback-authority",
"severity": "high",
"explanation": "Asset has an active clawback authority with no known association.",
"evidence": { "address": "CLAWBACKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }
}
],
"rulesetVersion": "2026-07-23.3"
}Failure and retry behavior
| Status | When |
|---|---|
| 400 | Malformed request body, invalid payment proof, or an unknown/expired nonce. |
| 401 / 403 | Missing or non-matching retrieval token on a status/report fetch. |
| 404 | Unknown payment id. |
| 429 | Rate limit exceeded. Response includes a Retry-After header and retry_after_seconds. Report requests and status polling use separate budgets, so polling your own payment doesn't compete with issuing a new one. |
Submitting the exact same X-Payment-Proof again is safe: it returns the current state of that payment instead of re-settling or rebuilding the report.
Report generation is retried automatically a few times before a payment is marked terminal. Until then, requests return 202. Once terminal, both status endpoints return 402 with one of these codes:
| failure_code | Meaning |
|---|---|
| PAYMENT_NOT_SETTLED | The payment gateway did not accept this payment. |
| VERIFICATION_INTERRUPTED | The server lost track of this payment's verification. Start a new payment. |
| REPORT_BUILD_FAILED | Report generation failed after repeated attempts. |
| REPORT_BUILD_TIMEOUT | Report generation repeatedly took too long. |
A terminal payment is not automatically retried further — contact support with the payment id.
Method and limits
The full ruleset — what each risk level means, how known-address labels work, and how incomplete holder data is handled — is documented in plain language on the methodology page.
- Known-address labels are curated associations, never endorsements.
- Incomplete holder data is stated explicitly, not silently rounded up.
UNKNOWNmeans the ruleset couldn't reach a conclusion — it is not the same asLOW.- Nothing in a report is investment advice.