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.

EndpointPOST /v1/reports
Paymentx402 (402 + signed proof)
CurrencyUSDC (Algorand ASA)
NetworkPer deployment — see below

Start 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.

GET /v1/config/public
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

  1. Request, no proofPOST /v1/reports with {"asset_id": <number>} and no X-Payment-Proof header.
  2. Read the challengeA 402 response body carries challenge: price, USDC asset id, pay-to address, nonce and expiry.
  3. Sign the paymentBuild and sign the payment with your own wallet — the challenge tells you what to sign.
  4. Retry with proofSame POST /v1/reports, now with X-Payment-Proof: <signed proof JSON>.
  5. Get a 202Normal outcome: a processing envelope with payment_id and retrieval_token. Generation runs in the background.
  6. Poll for the reportGET /v1/reports/:payment_id?token=<retrieval_token>, waiting retry_after_seconds between attempts, until it returns 200 or a terminal 402.

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:

FieldTypeNotes
asset_idintegerRequired. The Algorand ASA id to check.

Responses:

StatusMeaning
402No valid payment yet — body carries a payment challenge, or (with a proof) the gateway rejected the payment.
202Payment accepted; report generation is running in the background. Body carries payment_id and retrieval_token.
200Idempotent replay only — the proof matches an already-completed payment. Body is the report; X-Payment-Id and X-Retrieval-Token headers repeat the ids.
400Missing/invalid asset_id, malformed X-Payment-Proof JSON, or an unknown/expired challenge nonce.
403The submitted proof doesn't match the challenge it claims to answer.
429Rate limited — see Failure and retry behavior.
402 — payment challenge
{
  "error": "payment required",
  "challenge": {
    "priceUsdc": "0.01",
    "usdcAssetId": 10458941,
    "payTo": "<pay-to-address>",
    "nonce": "<challenge-nonce>",
    "expiresAt": "2026-07-24T12:05:00.000Z"
  }
}
202 — processing
{
  "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.

StatusMeaning
200Completed — full report body.
202Still processing — same envelope shape as above.
402Terminal failure — failure_code and a plain-language detail.
401No token supplied.
403Token doesn't match this payment.
404Unknown 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:

FieldWhat it holds
riskLevelLOW / MEDIUM / HIGH / UNKNOWN — see the methodology for how it's derived.
assetId, name, unit name, total supply, decimals, creator address, and the round the report was evaluated at.
controlRightsActive manager / freeze / clawback / reserve addresses (omitted if disabled or set to the zero address), defaultFrozen, and roleOverlaps.
recentConfigChangesConfiguration transactions found inside configChangeScanWindow (a concrete round range, not an unbounded "ever").
rekeyStatusisRekeyed, 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).
holderDistributionholdersScanned, an optional creatorSharePct, and dataIncomplete — see Method and limits.
findingsArray of { ruleId, severity, explanation, evidence } — every finding names the rule that produced it and the on-chain evidence behind it.
rulesetVersionVersion string of the rule catalogue that produced this report.
generatedAtISO timestamp.
disclaimerPlain-language reminder that this is technical evidence, not investment advice.
Report — abridged example
{
  "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

StatusWhen
400Malformed request body, invalid payment proof, or an unknown/expired nonce.
401 / 403Missing or non-matching retrieval token on a status/report fetch.
404Unknown payment id.
429Rate 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_codeMeaning
PAYMENT_NOT_SETTLEDThe payment gateway did not accept this payment.
VERIFICATION_INTERRUPTEDThe server lost track of this payment's verification. Start a new payment.
REPORT_BUILD_FAILEDReport generation failed after repeated attempts.
REPORT_BUILD_TIMEOUTReport 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.
  • UNKNOWN means the ruleset couldn't reach a conclusion — it is not the same as LOW.
  • Nothing in a report is investment advice.