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.

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

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

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
capabilitySummaryThe 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.
assetId, 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.
controlRightsActive manager / freeze / clawback / reserve addresses (omitted when cleared), defaultFrozen, and roleOverlaps.
recentConfigChangesEvery 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.
configChangeScanWindowWhat the history scan actually covered: fromRound, toRound, creationReached, scanLimitReached, eventsScanned, configChangesObserved, oldestScannedRound. incomplete is retained as a legacy alias for scanLimitReached || !creationReached.
rekeyStatusisRekeyed, 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).
findingsArray 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.
rulesetVersionVersion string of the rule catalogue that produced this report.
rulesetChangelogUrlWhere that version's rules are documented.
riskLevelLegacy. 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.
generatedAtISO timestamp.
disclaimerPlain-language reminder that this is technical evidence, not investment advice.
Report — abridged example (issuer that renounced freeze and clawback)
{
  "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

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 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, or unknown_or_incomplete.
  • Configuration history is scanned back to the asset's creation. creationReached says 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.