ASA Sentinel API
One endpoint returns a control 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 answers one question: what can this asset's issuer still do to the people holding it, and which of those powers has it permanently given up? The primary field is capabilitySummary — a plain statement for configuration control, freeze, clawback, reserve and supply, each with a durability state and the on-chain evidence behind it.
On Algorand a control address that has been cleared can never be reinstated, and clearing the manager address locks the whole configuration permanently. So the report separates a power the issuer happens not to be using from one it can never use again.
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 |
|---|---|
| capabilitySummary | The primary result. { statements, configurationMutable, permanentlyRenounced }. Each statement covers one capability (configuration, freeze, clawback, reserve, supply) with a state, a plain-language statement, a holderEffect when the power is actually held, an evidenceTier, and a basis naming the round and any renouncing transaction. |
| lifecycle | { status: "active" | "destroyed", createdAtRound?, destroyedAtRound? }. A destroyed asset's former role addresses are historical record and confer no current authority. |
| asset | Id, name, unit name, total supply, decimals, creator address, the round the report was evaluated at, and issuerAssertedUrl — the issuer's own on-chain URL, unverified. |
| controlRights | Active manager / freeze / clawback / reserve addresses (omitted when cleared), defaultFrozen, and roleOverlaps. |
| recentConfigChanges | Every configuration transaction the scan covered, each with the roles it actually changed. Includes changes made as inner transactions of an application call, flagged with fromInnerTransaction. A change whose parameters could not be read carries parametersUnavailable rather than being dropped. |
| configChangeScanWindow | What the history scan actually covered: fromRound, toRound, creationReached, scanLimitReached, eventsScanned, configChangesObserved, oldestScannedRound. incomplete is retained as a legacy alias for scanLimitReached || !creationReached. |
| rekeyStatus | isRekeyed, matched recentEvents, addresses confirmed rekeyed with no matching event in the scan window (rekeyedWithoutEvent), control addresses that don't exist as funded accounts (addressesNotFound), and addresses whose transaction scan was truncated (scanIncomplete). |
| findings | Array of { ruleId, severity, evidenceTier, explanation, evidence }. Subordinate to capabilitySummary — each adds a narrower observation on top of it. |
| freshness | { computedAt, checkedRound, servedFromCache }. When servedFromCache is true the report reused a recent build; the timestamp and round remain the original scan's. |
| rulesetVersion | Version string of the rule catalogue that produced this report. |
| rulesetChangelogUrl | Where that version's rules are documented. |
| riskLevel | Legacy. LOW / MEDIUM / HIGH / UNKNOWN, kept for backward compatibility. Not the report's answer: across a 3,000-asset mainnet sample it returns HIGH for roughly 92% of assets. Read capabilitySummary. |
| generatedAt | ISO timestamp. |
| disclaimer | Plain-language reminder that this is technical evidence, not investment advice. |
{
"assetId": 137594422,
"capabilitySummary": {
"configurationMutable": true,
"permanentlyRenounced": ["freeze", "clawback"],
"statements": [
{
"capability": "configuration",
"state": "active",
"statement": "Configuration can still be changed. An active manager address can reassign the reserve and manager addresses.",
"holderEffect": "The manager can reassign the reserve and manager addresses at any time, so the configuration shown below can still change. The freeze and clawback addresses are already cleared, and Algorand provides no way to reinstate a cleared control address, so the manager cannot restore those powers. The manager cannot change total supply or decimals.",
"address": { "address": "<manager-address>" },
"evidenceTier": "verified_on_chain_fact",
"basis": { "checkedRound": 63430401 }
},
{
"capability": "clawback",
"state": "permanently_renounced",
"statement": "No holding of this asset can ever be clawed back. The address is cleared, and Algorand provides no way to reinstate a cleared control address for the life of an asset. It was cleared at round 12853636.",
"evidenceTier": "derived_deterministic",
"basis": {
"checkedRound": 63430401,
"renouncedAtRound": 12853636,
"renouncedTxId": "<txid>"
}
},
{
"capability": "supply",
"state": "fixed_at_creation",
"statement": "Total supply is fixed. An ASA's total and decimals are set once at creation and cannot be altered afterwards, so the manager cannot mint additional units.",
"evidenceTier": "derived_deterministic",
"basis": { "checkedRound": 63430401 }
}
]
},
"lifecycle": { "status": "active", "createdAtRound": 12263663 },
"controlRights": {
"manager": { "address": "<manager-address>" },
"reserve": { "address": "<reserve-address>" },
"defaultFrozen": false,
"roleOverlaps": true
},
"configChangeScanWindow": {
"fromRound": 12263663,
"toRound": 63430401,
"creationReached": true,
"scanLimitReached": false,
"eventsScanned": 2,
"configChangesObserved": 1,
"incomplete": false
},
"findings": [
{
"ruleId": "recent-critical-config-change",
"severity": "high",
"evidenceTier": "derived_deterministic",
"explanation": "Control rights freeze, clawback changed at round 12853636.",
"evidence": { "txId": "<txid>", "round": 12853636, "changedRoles": ["freeze", "clawback"] }
}
],
"freshness": {
"computedAt": "2026-07-25T09:44:00.000Z",
"checkedRound": 63430401,
"servedFromCache": false
},
"riskLevel": "UNKNOWN",
"rulesetVersion": "2026-07-25.2",
"rulesetChangelogUrl": "https://asasentinel.com/methodology#ruleset-changelog"
}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 method — what each capability state means, the four evidence tiers, how history coverage is established, and how known-address labels work — is documented in plain language on the methodology page.
- Every claim carries an
evidenceTier:verified_on_chain_fact,derived_deterministic,known_association, orunknown_or_incomplete. - Configuration history is scanned back to the asset's creation.
creationReachedsays whether it got there; a bounded scan is never described as complete. - An incomplete scan does not weaken a permanence statement — those rest on the asset's configuration at the checked round plus a protocol rule, not on history. What it costs is the ability to say when a power was given up.
- Known-address labels are curated associations, never endorsements.
- Holder distribution is not reported. A complete holder set cannot be retrieved inside a request budget, and a capped sample supports neither an honest holder count nor a creator share.
- ASA Sentinel does not determine whether an asset is safe, legitimate, approved, or a good investment. It reports what the issuer can technically do.
- Nothing in a report is investment advice.