Formal specification for the attestation format, signing scheme, and verification algorithm used by InsumerAPI.
This document specifies the attestation format, signing scheme, and verification algorithm used by InsumerAPI, the on-chain attestation authority for AI agents, apps, and commerce.
An InsumerAPI attestation is a cryptographically signed boolean assertion ("wallet X satisfies condition Y at block Z") that any downstream system can independently verify without re-querying a blockchain node or learning the underlying state values. Every attestation carries an ECDSA P-256 signature. Since 1 September 2026 it also carries, whenever post-quantum signing is configured, an additive ML-DSA-65 post-quantum signature over a post-quantum domain tag followed by the same preimage (Section 12, Check 6).
This specification defines:
This document is intended for integrators and verifiers who consume InsumerAPI responses. It defines everything needed to parse, verify, and trust an attestation.
| Term | Definition |
|---|---|
| Attestation | A signed statement about on-chain state produced by InsumerAPI |
| Verifier | Any party that checks the cryptographic validity of an attestation |
| Condition | A predicate over on-chain state (e.g., "balance >= threshold") |
| Condition hash | SHA-256 digest of the canonical JSON of an evaluated condition |
| Block anchor | The block number and timestamp at which state was read |
| Merkle proof | A cryptographic storage proof binding a value to a block header |
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
┌──────────────┐
│ Blockchain │
│ State │
└──────┬───────┘
│
│
┌──────────┐ conditions + wallet ┌──┴───────────┐ signed attestation ┌──────────┐
│ Caller │ ────────────────────────▶ │ InsumerAPI │ ──────────────────────▶ │ Verifier │
└──────────┘ └──────────────┘ └──────────┘
│
fetch JWKS
│
┌──────┴───────┐
│ JWKS │
│ Endpoint │
└──────────────┘POST /v1/attest).All InsumerAPI responses are signed using ECDSA with the P-256 curve (secp256r1) and SHA-256 (JOSE algorithm identifier ES256).
The sig field contains a base64-encoded (standard alphabet) P1363-format signature (two 32-byte integers r || s, total 64 bytes). The encoded signature is 88 characters.
P1363 is the fixed-length format (not DER). Verifiers MUST use P1363 decoding.
For an attestation, the signed payload is the JSON serialization of the following fields in this order:
{"id":"...","pass":true,"results":[...],"attestedAt":"..."}The field order is fixed: id, pass, results, attestedAt. This is the exact output of JSON.stringify() on the attestation object as constructed by InsumerAPI.
Every other field of the attestation object is outside the signed payload: passCount, failCount, expiresAt, and any field not listed above. Anyone holding a response can change them without affecting the signature. expiresAt is bound to the signed attestedAt by Check 4, and passCount and failCount are recounted from the signed results by Check 2a (Section 12). A relying party treats no other unsigned field as part of the attestation.
For a trust profile, the signed payload is the JSON serialization of the trust object with fields in this order: id, wallet, conditionSetVersion, dimensions, summary, profiledAt, expiresAt.
The digest is SHA-256(json_bytes), and the signature is computed over this 32-byte digest.
Note: The v1 signed payload uses insertion-order JSON serialization, not alphabetically sorted keys. This is distinct from condition hashes (Section 8), which use sorted keys.
Keys created on or after the v2 rollout sign a domain-separated preimage. For these keys the signature is computed over:
attestation: "insumer.attestation.v2" + "\n" + canonical_json({"v":2,"id":"...","pass":true,"results":[...],"attestedAt":"..."})
trust profile: "insumer.trust.v2" + "\n" + canonical_json({"id":"...","wallet":"...","conditionSetVersion":"2026-10-08","dimensions":{...},"summary":{...},"profiledAt":"...","expiresAt":"..."})where the attestation preimage carries a leading "v":2 member and the trust preimage is the entire trust object exactly as returned (the same seven fields as Section 3.3, expiresAt included, no "v" member), and canonical_json is the JCS / RFC 8785 canonical form (recursively sorted keys, Section 8) rather than insertion order. The domain tag binds each signature to its message type, so an attestation signature cannot be reinterpreted under another context. Under v2 the threshold of a token_balance or ratio_to_amount condition, and the ratio quantities, are canonical decimal strings with no decimals field, so the signed bytes are exact and reproducible by any verifier in any language. nft_ownership conditions and the curated trust checks keep threshold as the JSON number 0 (Sections 7 and 14.1).
v1 keys continue to sign the insertion-order payload in Section 3.3 with no domain tag (see Section 14, Versioning). The kid on the response selects both the key and the scheme.
Every signed response includes a kid (Key ID) string. Verifiers use this value to select the correct public key from the JWKS endpoint.
InsumerAPI publishes its signing keys as a JSON Web Key Set (RFC 7517) at two locations:
GET https://api.insumermodel.com/v1/jwks (24-hour cache)https://insumermodel.com/.well-known/jwks.json (also served at https://api.insumermodel.com/.well-known/jwks.json)The JWKS document holds five entries over two keys. The first three are the ECDSA P-256 key under three kid values; the last two are RFC 9964 AKP entries for the ML-DSA-65 post-quantum key (Section 12, Check 6). The EC entries carry:
| Field | Value | Description |
|---|---|---|
kty | "EC" | Key type |
crv | "P-256" | Curve |
x | base64url | x-coordinate of the public key |
y | base64url | y-coordinate of the public key |
use | "sig" | Key usage |
alg | "ES256" | Algorithm |
kid | "insumer-attest-v2" | Key identifier. Three IDs point at the same EC key: insumer-attest-v1 (legacy attest + trust), insumer-attest-v2 (v2 attest), and insumer-trust-v2 (v2 trust). The kid on each response selects both the key and the signing scheme (Section 3.3.1) |
The two AKP entries, appended after the EC entries so that a verifier that falls back to the first key still meets a classical one, carry:
| Field | Value | Description |
|---|---|---|
kty | "AKP" | Algorithm Key Pair (RFC 9964) |
alg | "ML-DSA-65" | FIPS 204 parameter set |
pub | base64url | The 1952-byte ML-DSA-65 public key |
use | "sig" | Key usage |
kid | "insumer-attest-pq1" / "insumer-trust-pq1" | Selected by the response's pqKid. Both point at the same key; the label names the domain tag the post-quantum signature signs under. |
Keys are never removed from the JWKS document. If a key rotation occurs, the previous key stays in the JWKS document permanently under its original kid, so the signature on an attestation made with it stays checkable for as long as anyone holds the attestation as a record. This does not extend the attestation's validity: once its expiresAt has passed, a verifier applying Check 4 rejects it as expired. Verifiers SHOULD select the key matching the response's kid rather than hardcoding a key. Relying parties that keep attestations as records SHOULD also keep a copy of the JWKS document and of /.well-known/pq-key-binding.json with them, so the records can be verified without depending on these endpoints.
An attestation is the core output of the protocol. It asserts whether a wallet satisfies a set of conditions at a specific point in time.
{
"id": "ATST-A7C3E1B2D4F56789",
"pass": true,
"results": [ ... ],
"passCount": 2,
"failCount": 0,
"attestedAt": "2026-02-26T12:34:57.000Z",
"expiresAt": "2026-02-26T13:04:57.000Z"
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | REQUIRED | Unique identifier. Format: ATST- followed by 16 uppercase hex characters. |
pass | boolean | REQUIRED | true if and only if ALL conditions are met. |
results | array | REQUIRED | Per-condition results (Section 5.2). |
passCount | integer | REQUIRED | Count of conditions where met is true. Not part of the signed payload (Section 3.3); a verifier recounts it from results (Section 12, Check 2a). |
failCount | integer | REQUIRED | Count of conditions where met is false. Not part of the signed payload (Section 3.3); a verifier recounts it from results (Section 12, Check 2a). |
attestedAt | string | REQUIRED | ISO 8601 timestamp when the attestation was created. |
expiresAt | string | REQUIRED | ISO 8601 timestamp after which the attestation SHOULD be considered stale. 30 minutes after attestedAt, or 5 minutes when any result carries an erc7710_delegation condition (Section 7.2). Not part of the signed payload (Section 3.3); see Check 4 in Section 12. |
pqSig | string | OPTIONAL | Sibling of sig (in data, not inside the attestation object): base64 ML-DSA-65 post-quantum signature over the post-quantum domain tag plus the classical preimage (Section 12, Check 6). Present when post-quantum signing is configured. |
pqKid | string | OPTIONAL | Sibling of kid: the post-quantum key identifier (insumer-attest-pq1) resolved in the JWKS. |
pqJwt | string | OPTIONAL | Sibling of jwt when format: "jwt" is requested: a compact JWS with alg "ML-DSA-65". A verifier binds it to jwt by the whole claim set, compared as parsed JSON and never by bytes (Section 12, Check 6). |
Each entry in the results array describes the evaluation of one condition:
| Field | Type | Required | Description |
|---|---|---|---|
condition | integer | REQUIRED | Zero-based index of this condition in the request. |
label | string | REQUIRED | Human-readable label provided by the caller, carried inside the signed result. Always present: when the request sends none it is an empty string, or a default for some types ("Farcaster ID" for farcaster_id, "Registered ERC-8004 agent" for erc8004_agent, "Authorized by principal" for erc7710_delegation, and a template’s own label for a templated eas_attestation). A label longer than 100 UTF-16 code units is cut rather than rejected; the cut never splits a surrogate pair, and an unpaired surrogate becomes U+FFFD, so a signed label is always well-formed. Under v2 the label is NFC-normalized before signing, so its bytes can differ from the bytes the request sent. |
type | string | REQUIRED | Condition type (Section 6). |
chainId | integer or string | REQUIRED | Chain identifier. |
met | boolean | REQUIRED | Whether the condition was satisfied. |
evaluatedCondition | object | REQUIRED | The exact predicate that was evaluated (Section 7). |
conditionHash | string | REQUIRED | 0x-prefixed SHA-256 hex digest of the canonical JSON of evaluatedCondition (Section 8). |
blockNumber | string | CONDITIONAL | Hex-encoded block number. Present on every EVM chain. |
blockTimestamp | string | CONDITIONAL | ISO 8601 timestamp of the block. Present if and only if blockNumber is present. |
ledgerIndex | integer | CONDITIONAL | Ledger index. Present for XRPL and Stellar conditions. |
slot | integer | CONDITIONAL | Solana slot. Present only for Solana conditions. |
blockHeight | integer | CONDITIONAL | Block height. Present for Bitcoin and Tron conditions. |
blockHash | string | CONDITIONAL | Block hash. Present for Bitcoin and Tron conditions. |
checkpointSequence | integer | CONDITIONAL | Sui checkpoint sequence number. Present only for Sui conditions. |
ledgerHash | string | CONDITIONAL | Validated ledger hash. Present for XRPL and Stellar conditions when the ledger hash is available. Enables independent snapshot verification. |
trustLineState | object | CONDITIONAL | Trust line state flags. Present for non-native XRPL token_balance conditions when the trust line exists. Contains frozen (boolean), which is true when the trust line is frozen or when its issuer has frozen everything it issued. Either causes met: false. |
proof | object | OPTIONAL | Merkle storage proof (Section 10). Present only when requested. Inside the signed results, outside conditionHash. |
| Type-specific fields | various | CONDITIONAL | totalSupply and, under v1, fraction (Section 6.6); agentExists and matchedVia (Section 7.1); declaredLimits, declaredLimitsOmitted, unrecognizedEnforcers and failReason (Section 7.2). Like proof, they sit inside the signed results and outside conditionHash. |
The complete response pairs the attestation object with its signature:
{
"ok": true,
"data": {
"attestation": { ... },
"sig": "<88-char base64, P1363 r||s>",
"kid": "insumer-attest-v2",
"pqSig": "<base64 ML-DSA-65 signature>",
"pqKid": "insumer-attest-pq1"
},
"meta": {
"version": "1.0",
"timestamp": "2026-02-26T12:34:57.000Z"
}
}The sig and kid fields MUST be siblings of the object they sign (not nested within it), and so are the post-quantum signature fields pqSig and pqKid (Section 12, Check 6). The meta object is NOT part of the signed payload.
Callers MAY request the attestation as a standard JWT by including "format": "jwt" in the request body. When present, the response’s data object carries an additional jwt member beside attestation, sig and kid, and a pqJwt beside pqSig (Section 12, Check 6).
The JWT is signed with ES256 (ECDSA P-256 + SHA-256) using the same key identified by kid. The header and claims are:
Header:
| Field | Value |
|---|---|
alg | ES256 |
typ | JWT |
kid | The same kid as the response: insumer-attest-v2, or insumer-attest-v1 for a v1 key |
Claims:
| Claim | Type | Description |
|---|---|---|
iss | string | https://api.insumermodel.com |
sub | string | The wallet a condition in the request evaluated. A wallet field that no condition reads is ignored and never named. When conditions span chain families, sub names the first evaluated family in the order wallet, solanaWallet, xrplWallet, bitcoinWallet, tronWallet, stellarWallet, suiWallet; the other families’ wallets are evaluated but not named by sub. |
jti | string | The attestation ID (e.g. ATST-A7C3E1B2D4F56789). |
iat | integer | Issued-at timestamp (Unix seconds). |
exp | integer | Expiration timestamp (Unix seconds). Default: iat + 1800; it matches the attestation's expiresAt, so an attestation carrying an erc7710_delegation condition carries iat + 300. |
pass | boolean | Aggregate pass/fail; same as attestation.pass. |
results | array | Per-condition results; same as attestation.results. |
conditionHash | array | Array of 0x-prefixed SHA-256 hex strings; one per condition in results. |
blockNumber | string | Hex-encoded block number from the first result (when available). For multi-chain attestations, per-result block info is inside the results array. |
blockTimestamp | string | ISO 8601 block timestamp from the first result (when available). |
The JWT is verifiable by any standard JWT library (Kong, Nginx, Cloudflare Access, AWS API Gateway) using the JWKS endpoint at GET /v1/jwks or the static /.well-known/jwks.json.
A condition is a predicate evaluated against on-chain state. InsumerAPI supports ten condition types. The four core types are documented first. evm_view_call (an arbitrary bool-returning view function, named by a canonical selector of the form functionName(address), on EVM chains) is an additive type. ratio_to_amount and ratio_to_supply (dimensionless ratio conditions, where the threshold is derived from a multiple of a reference amount or a share of total supply) are additive v2 types; see the signature-scheme section for their canonical decimal-string quantity encoding. erc8004_agent and erc7710_delegation (agent conditions: ERC-8004 agent registration and ERC-7710 delegation validity, Base only at launch) are additive types, generally available on any API key and also reachable with no key via x402 pay-per-call. Their evaluatedCondition layouts are given in Section 7. account_code (Section 6.8) asks what code sits at the wallet address itself: a plain key, an EIP-7702 delegation designator, or contract code; its layout is given in Section 7.3.
token_balanceAsserts whether a wallet's balance of a token, or of the chain's native coin, meets a threshold.
| Parameter | Type | Required | Description |
|---|---|---|---|
type | "token_balance" | REQUIRED | |
contractAddress | string | REQUIRED | The asset. On EVM chains, an ERC-20 contract address or "native" for the chain's own coin. On Solana, a mint or "native"; on XRPL, the issuer's r-address or "native"; on Tron, a TRC-20 address or "native"; on Stellar, the issuer's G-address or "native"; on Sui, a coin type such as 0x2::sui::SUI ("native" is not accepted there); on Bitcoin, "native" only. Signed as sent (Section 8.3) |
chainId | integer or string | REQUIRED | An EVM chain ID, or one of "solana", "xrpl", "bitcoin", "tron", "stellar", "sui" |
currency | string | CONDITIONAL | XRPL only. Required for a trust-line token (an issuer other than "native"). "XRP" is not a trust-line currency. Signed in normalized form (Section 8.3) |
taxon | integer 0–4294967295, or its digits as a string | OPTIONAL | XRPL only. Accepted and signed when supplied, but it does not affect a token_balance verdict; it applies to nft_ownership (Section 6.2) |
assetCode | string | CONDITIONAL | Stellar only. Required for a trust-line asset (an issuer other than "native"): 1 to 12 letters and digits. Signed as sent |
threshold | string (v2 keys) or number (v1 keys) | REQUIRED | Minimum balance in human-readable units, as a decimal string on every key issued since the v2 rollout (Section 14.1) |
decimals | integer 0–100, or its digits as a string | OPTIONAL | Token decimals, as a cross-check. The scale is always the token's own decimals (on EVM chains, read at the anchored block); it is never taken from the request and never assumed. On EVM chains, Tron and Sui a value that differs from the token's own is rejected with a 400 that names the token's own value. On Solana, XRPL, Stellar and Bitcoin it is checked for range and otherwise not used. On EVM chains and Sui a token that states no usable decimals is signed false when the wallet's balance is zero, since nothing needs comparing, and is rejected with a 400 otherwise |
label | string | OPTIONAL | Human-readable label |
Semantics: met is true when the wallet's balance (adjusted for decimals) is greater than or equal to threshold.
Comparison (operator field): "gte"
nft_ownershipAsserts whether a wallet holds at least one NFT from a collection.
| Parameter | Type | Required | Description |
|---|---|---|---|
type | "nft_ownership" | REQUIRED | |
contractAddress | string | REQUIRED | An ERC-721 contract address on EVM chains, a mint on Solana, or the issuer's r-address on XRPL. "native" is not an NFT and is rejected with a 400 |
chainId | integer or string | REQUIRED | An EVM chain ID, "solana" or "xrpl" |
taxon | integer 0–4294967295, or its digits as a string | OPTIONAL | XRPL only. Limits the check to NFTs carrying this taxon. Signed when supplied |
label | string | OPTIONAL | Human-readable label |
Semantics: met is true when the wallet holds one or more tokens from the collection.
Chains: Tron, Stellar and Sui support token_balance only, and Bitcoin supports token_balance with "native" only. Any other combination is rejected with a 400.
Comparison (operator field): "gt". Evaluated threshold: 0
eas_attestationAsserts whether a wallet has received a valid Ethereum Attestation Service attestation matching a schema.
| Parameter | Type | Required | Description |
|---|---|---|---|
type | "eas_attestation" | REQUIRED | |
schemaId | string | CONDITIONAL | Bytes32 hex EAS schema ID. Required unless template is provided. |
attester | string | OPTIONAL | Expected attester address. If provided, only attestations from this address are accepted. |
template | string | OPTIONAL | Named compliance template (pre-configured shorthand). |
chainId | integer | CONDITIONAL | Required when using raw schemaId: one of 1, 10, 137, 8453, 42161. |
indexer | string | CONDITIONAL | Required when using raw schemaId: the contract that resolves the attestation for this wallet and schema. Not part of the signed condition. |
label | string | OPTIONAL | Human-readable label |
Semantics: met is true when a valid, non-revoked attestation exists for the wallet matching the specified schema and attester constraints. The schema named in the signed condition is checked against the attestation that is found, so the choice of indexer cannot change what the signature states. An attestation's expiry is judged at the anchored block's timestamp.
Comparison (operator field): "valid" (standard) or "decoder" (template-specific evaluation logic)
farcaster_idAsserts whether a wallet is registered on Farcaster.
| Parameter | Type | Required | Description |
|---|---|---|---|
type | "farcaster_id" | REQUIRED | |
label | string | OPTIONAL | Human-readable label |
Semantics: met is true when the wallet has a registered Farcaster ID. The condition is always read on Optimism and signed with chainId 10; a chainId in the request is not used.
Comparison (operator field): "registered"
evm_view_callAsserts whether an arbitrary contract's own view function answers true for the wallet. This is the general-purpose escape hatch: any predicate a contract already exposes (hasAccess(address), isMember(address), an allowlist, a custom policy) becomes attestable without a dedicated condition type.
| Parameter | Type | Required | Description |
|---|---|---|---|
type | "evm_view_call" | REQUIRED | |
contractAddress | string | REQUIRED | The contract to call. A real deployed contract; "native" is not valid for this type. |
chainId | integer | REQUIRED | An EVM chain. Non-EVM chains are not supported. |
selector | string | REQUIRED | The canonical signature of the view function, in the form functionName(address). v1 supports single-address-argument view functions only; the signature must match ^[a-zA-Z_$][a-zA-Z0-9_$]*\(address\)$. |
label | string | OPTIONAL | Human-readable label |
Semantics: InsumerAPI derives the 4-byte function selector as the first 4 bytes of keccak256 of the canonical signature, forms calldata as that selector followed by the wallet address left-padded to 32 bytes, and calls the view function on contractAddress at the anchored block. met is true when the call returns a nonzero 256-bit word (the ABI encoding of true). A revert or an empty return is an on-chain fact and produces a signed false; a transport failure refuses to sign rather than guessing.
Comparison (operator field): "view_call_true"
Reproducing the verdict: The evaluatedCondition carries the canonical selector string, so a verifier can recompute the 4-byte selector, repeat the same call at the anchored blockNumber, and compare. No information beyond the signed result is needed.
Scope of the claim: The attestation states what the named contract returned at the anchored block, and nothing more. The contract's own correctness, semantics, and upgradeability are outside the signature's scope: the signed bytes commit to the contract, the selector, the chain, and the block. Merkle storage proofs are not available for this type, since the predicate is the contract's computation rather than a single storage slot.
ratio_to_amount and ratio_to_supplyTwo dimensionless balance rules share the token_balance anchor and hashing rules. ratio_to_amount is met when the balance is at least multiple times amount; the derived threshold travels in evaluatedCondition.threshold with operator "gte". Under v2 it is multiple times amount rounded up to the token's smallest unit (at a fixed scale of 18 when the token states no decimals), so a verifier recomputes it from the two inputs and the token's decimals and re-reads the balance at the anchored block; under v1 it is the plain product of the two numbers. ratio_to_supply is met when balance divided by the token's totalSupply() at the anchored block is at least minFraction, a value in the open-closed interval (0, 1]; its evaluatedCondition carries minFraction with operator "gte_fraction". The raw supply read is returned as totalSupply (absent when totalSupply() reverts or the address holds no code, which is a signed false), a sibling of evaluatedCondition on the result: inside the signed payload, outside conditionHash, so a verifier re-reads balance and supply at the pinned block and re-derives the boolean exactly. Under v1 the quantities are JSON numbers and the result also carries the computed fraction; under v2 they are canonical decimal strings and no fraction is emitted (Section 14.1). Native assets have no totalSupply() and are rejected for ratio_to_supply. Both ratio types are available on EVM chains only. Under v2, an amount with more decimal places than the token's own decimals, or a multiple with more than 18, is rejected with a 400.
A request outside these limits is rejected with a 400 and nothing is read or signed:
erc7710_delegation.terms per caveat and a signature of at most 4 KB. Only the root authority is accepted; delegation chains are not.agentId of at most 78 decimal digits.threshold, multiple, amount, minFraction) of at most 400 characters, in the form given in Section 8.3.threshold, multiple or amount greater than zero.account_codeAsserts the code state of the wallet address itself at the anchored block. Three states exist on a chain and they are exclusive: no code (a plain key account), the EIP-7702 delegation designator (a key that has delegated execution to a contract), or any other code (a smart-contract wallet, a protocol, a token). The condition names one state and the attestation says whether the address is in it.
| Parameter | Type | Required | Description |
|---|---|---|---|
type | "account_code" | REQUIRED | |
chainId | integer | REQUIRED | An EVM chain. A non-EVM chain is refused with a 400. |
expect | string | REQUIRED | The state the address must be in: "none" (no code), "eip7702" (the delegation designator) or "contract" (any other code). |
delegate | string | OPTIONAL | With expect: "eip7702" only; a 400 with any other expect. An EVM address. When supplied, the condition is met only if the designator points at this address. |
label | string | OPTIONAL | Human-readable label |
Semantics: The code at the wallet address is read at the anchored block and classified into one of the three states. met is true when that state is the one named by expect, and, when delegate is supplied, when the designator's target is that address. A read that does not complete refuses to sign; it is never reported as met: false.
Comparison (operator field): "code_state"
Scope of the claim: The result is the boolean. The code itself and the delegation target are never returned, in any format or mode; a caller who cares which contract names it in delegate and still receives only yes or no. Which contract stands behind contract code is outside the signed statement. With proof: "merkle" the result carries an account proof whose codeHash is the proven value (Section 10.2). A trust profile reports the same two facts without a target in its account dimension (Section 11.3).
Every result includes an evaluatedCondition object that captures the exact predicate that was applied. This is a statement of what was checked, not a repeat of the caller's input.
The evaluated condition MUST include:
| Field | Present when | Description |
|---|---|---|
type | Always | Condition type identifier |
chainId | Always | Chain where state was read |
contractAddress | token_balance, nft_ownership, evm_view_call, ratio_to_amount, ratio_to_supply | Contract or asset that was queried, as sent (Section 8.3) |
operator | Always | Comparison operator: gte, gt, valid, decoder, registered, gte_fraction (ratio_to_supply), view_call_true (evm_view_call), owner_or_bound_wallet (erc8004_agent), authorized_by_principal (erc7710_delegation), code_state (account_code) |
threshold | token_balance, nft_ownership, ratio_to_amount | Threshold used (human-readable units): a decimal string under v2, a number under v1; nft_ownership carries the number 0 under both. On ratio_to_amount it is the derived threshold (Section 6.6) |
multiple, amount | ratio_to_amount | The two inputs the threshold is derived from |
minFraction | ratio_to_supply | Minimum share of total supply |
decimals | token_balance, ratio_to_amount, under v1 only | Token decimals. Where the API cross-checks the scale (EVM chains, Tron, Sui) it never differs from the token's own, except that a token stating no decimals at all (signed false, Section 6.1) carries a fixed 18 on EVM chains and 9 on Sui; elsewhere it is the value the request sent, or null. Absent under v2: the decimal-string threshold carries no separate scale (Section 8.2) |
selector | evm_view_call | Canonical view-function signature the 4-byte selector derives from (Section 6.5) |
schemaId | eas_attestation, except decoder-based templates | Schema that was checked |
attester | eas_attestation (when filtered) | Attester address filter |
decoder | eas_attestation (template-specific) | Decoder contract address used for template evaluation (e.g., Gitcoin Passport) |
decoderMethod | eas_attestation (with decoder) | The decoder method that decides met: isHuman or getScore |
currency | XRPL conditions (when supplied; always on trust line conditions) | Currency code for XRPL trust line checks, in normalized form (Section 8.3) |
assetCode | Stellar trust-line conditions | Asset code of the trust-line asset |
taxon | XRPL conditions (when supplied) | NFT taxon filter for XRPL NFT conditions |
expect | account_code | The code state the condition asked for: none, eip7702 or contract (Section 7.3) |
delegate | account_code (when supplied); also on erc7710_delegation (Section 7.2) | On account_code, the address the caller named, signed in lower case; met then also requires the designator to point at it |
The evaluated condition serves two purposes:
erc8004_agent layoutThe evaluatedCondition for an erc8004_agent condition carries exactly:
{
"type": "erc8004_agent",
"chainId": 8453,
"registry": "0x8004a169fb4a3325136eb29fa0ceb6d2e539a432",
"agentId": "123",
"wallet": "0x<attested wallet>",
"operator": "owner_or_bound_wallet"
}The registry and wallet are signed in lower case. met is true iff the attested wallet either owns the agent NFT (ownerOf) or is the registry's on-chain signature-verified agentWallet binding (getAgentWallet). The result object additionally carries agentExists (boolean) and matchedVia ("owner" | "agent_wallet" | "none") inside the signed results array, outside conditionHash. ERC-8004 registration is permissionless NFT minting: the signed statement is exactly “registered in registry R at agentId N, owned by / bound to this wallet”. It implies no vetting, no reputation, no endorsement. The registry clears the agentWallet binding automatically when the agent NFT transfers, so a block-anchored verdict reflects binding truth at that block.
erc7710_delegation layoutThe evaluatedCondition for an erc7710_delegation condition carries exactly:
{
"type": "erc7710_delegation",
"chainId": 8453,
"delegationManager": "0xdb9b1e94b5b69df7e401ddbede43491141047db3",
"domainVersion": "1",
"frameworkVersion": "1.3.0",
"delegationHash": "0x<hash>",
"delegator": "0x<principal>",
"delegate": "0x<agent wallet>",
"wallet": "0x<attested wallet>",
"authority": "root",
"expectedDelegator": "0x<principal>",
"signatureType": "eoa",
"caveatCoverage": "full",
"caveatCount": "1",
"unevaluatedCaveats": "0",
"operator": "authorized_by_principal"
}Every address in this layout is signed in lower case. signatureType is one of "eoa", "erc1271", or "none". "eoa" means an EOA principal signed the delegation; "erc1271" means a smart-contract principal asserts the delegation is valid. That is a different claim, and both appear distinctly in the signed bytes. caveatCoverage is "full" or "partial". caveatCount and unevaluatedCaveats are decimal strings inside the signed bytes, never JSON numbers. The result object additionally carries declaredLimits (decoded caveat limits with decimal-string amounts in base units), and on failure unrecognizedEnforcers and failReason (delegate_mismatch, principal_mismatch, invalid_signature, delegator_not_deployed, revoked, unknown_caveat_enforcer, outside_time_window). A recognized enforcer whose terms cannot be decoded is treated as unrecognized: it is listed in unrecognizedEnforcers and the verdict fails with unknown_caveat_enforcer. delegator_not_deployed is reported when the principal address holds no contract code at the anchored block, which is the state a smart-contract wallet is in until its first transaction. It is distinct from invalid_signature because no signature was evaluated: there was no principal on chain to evaluate one against. The verdict is still a signed false, since redemption calls into the delegator account and an undeployed principal cannot exercise the delegation. Declared limits are reported, not simulated: on-chain redemption enforces them, and the attestation states what the principal signed. Attestations containing a delegation condition expire in 5 minutes rather than the standard 30: revocation is one transaction away, so the verdict window stays tight, and the verdict states “not revoked as of block N” (blockNumber is in the signed result). That clause is itself provable: a delegation condition requested with proof: "merkle" returns an EIP-1186 storage proof of the revocation slot, specified in Section 10.7.
met is the verdict, and it is exactly this boolean: the attested wallet is the delegate, the declared delegator matches the required expectedDelegator, the EIP-712 signature verifies, the delegation is unrevoked as of the anchored block, every caveat uses a recognized enforcer, and any time-window caveat is satisfied at the anchored block's timestamp (strictly after timestampAfter and strictly before timestampBefore, where 0 means no bound on that side). declaredLimits is not an input to that boolean. It is a signed decode of the caveat terms the caller itself submitted, and it sits as a sibling of evaluatedCondition, inside the signed results payload but outside conditionHash. A verifier reading a delegation attestation should treat met as the statement and declaredLimits as signed context riding alongside it. That context rides on affirmative and negative verdicts alike: the decode is computed once the signature and revocation checks pass, so a delegation failing on unknown_caveat_enforcer or outside_time_window still carries the decode of its recognized caveats. For a time-window failure, the decoded timestamp caveat is the evidence for the failure. Failures at or before the signature and revocation checks (delegate_mismatch, principal_mismatch, invalid_signature, delegator_not_deployed, revoked) carry no decode.
Each entry carries the exact hex bytes it was decoded from, so the decode is independently checkable:
{
"kind": "erc20_transfer_amount",
"enforcer": "0xf100b0819427117ecf76ed94b358b1a5b5c6d2fc",
"terms": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000000000000000003b9aca00",
"token": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"maxAmount": "1000000000"
}kind is one of timestamp (fields timestampAfter, timestampBefore), erc20_transfer_amount (token, maxAmount), native_transfer_amount (maxAmount), allowed_targets (targets), or limited_calls (maxCalls). All numeric values are decimal strings in base units. Because the caller supplied terms in its own request, verification requires no external lookup: re-run the enforcer contract's public getTermsInfo byte layout against terms and compare it to the decoded values, then recompute delegationHash (the EIP-712 struct hash inside evaluatedCondition, which commits to every caveat's enforcer and terms) from the delegation object held locally and confirm it matches. The hash pins which caveats were evaluated; the byte layout pins what each one says.
A delegation attestation in JWT form is portable, so by default the principal's declared limits travel to anyone the attestation is forwarded to. That is usually desirable, since a relying party needs the limits to act on them, but not always. The top-level request modifier declaredLimits (a sibling of proof and format) takes "include" (the default, also the behavior when the field is absent) or "omit". Under "omit" the decoded limits are not included and the result carries declaredLimitsOmitted: true in their place. The caller loses nothing: it submitted the caveats and already holds them.
The modifier changes only what rides alongside the verdict. met, delegationHash, and conditionHash are byte-identical whether or not the limits are omitted, since declaredLimits was never inside evaluatedCondition to begin with. A forwarded attestation therefore still commits to exactly which delegation was checked, and a holder can verify a limit shown to them out-of-band without learning one from the attestation itself.
account_code layoutThe evaluatedCondition for an account_code condition carries exactly:
{
"type": "account_code",
"chainId": 8453,
"expect": "eip7702",
"operator": "code_state"
}When the request supplied delegate, the object also carries "delegate": "0x<address>", signed in lower case (Section 8.3). Nothing read from the chain is echoed: the code and the designator's target never appear. The condition hash (Section 8) is computed over this object under both signing schemes; the object above hashes to 0x6c5752bfbfcfd6ba36c9cda6c74df567f0e0414da6b7a3176061ba734aeadc46. A trust profile's account rows carry this same layout with expect "contract" or "eip7702" (Section 11.3), so a row's conditionHash equals the hash of the equivalent attest condition.
Each result MUST include a conditionHash field computed as:
conditionHash = "0x" + hex(SHA-256(canonical_json(evaluatedCondition)))
Where canonical_json produces a JSON string with:
, and :)A verifier MUST recompute the condition hash (Section 12, Check 2) from the evaluatedCondition object and compare it to the claimed conditionHash. If they differ, the result MUST be rejected as tampered.
Because the conditionHash is embedded in the results array, which is part of the signed payload, a valid signature over the attestation transitively guarantees the integrity of every evaluated condition.
A token_balance condition on Ethereum mainnet USDC ("holds at least 1000 USDC"), as a v2 attestation carries it. Its canonical form is the exact byte string the hash is computed over:
{"chainId":1,"contractAddress":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","operator":"gte","threshold":"1000","type":"token_balance"}The condition hash is 0x + the hex-encoded SHA-256 of those bytes:
0x630c213d362cd4e617261c2475f7b8ad70096ac911db804392305d6cbf70f094
Both values are reproducible: canonicalize the object, take the SHA-256, and the digest above follows. The threshold is a decimal string in display units, exactly as the condition stated it: "1000" is 1000 USDC. The signed bytes carry no separate scale, because the scale is never an input: it is always the token's own decimals, read from the chain at the anchored block (Section 6.1).
v1: keys issued before the v2 rollout (Section 3.3) carry threshold as a JSON number alongside a decimals field, so the same condition canonicalizes as {"chainId":1,"contractAddress":"0xA0b8...eB48","decimals":6,"operator":"gte","threshold":1000,"type":"token_balance"} and hashes to a different digest. The hashing algorithm is identical.
The ratio quantities (multiple, amount, minFraction) follow the same rule as threshold: canonical decimal strings under v2, JSON numbers with decimals under v1.
A relying party that pins a conditionHash pins exact bytes, so it needs to know how each identifier is written into the signed condition.
contractAddress is signed as sent. It keeps the letter case the request used (an EVM address in checksum case and the same address in lower case produce different hashes), and "native" keeps the case it was sent in. The one rewrite is an xdc prefix on a token_balance, nft_ownership or ratio condition, signed as 0x; an evm_view_call address must already start with 0x.assetCode, an EAS schemaId and attester, and an evm_view_call selector are signed as sent.threshold, multiple, amount, minFraction) must be digits with an optional fractional part: no sign, no exponent, no leading point. It is signed in canonical form, with leading zeros and trailing fractional zeros removed and no point on a whole number, so "1000.0" is signed as "1000" and "00.50" as "0.5". A JSON number in these fields is rejected with a 400 on a v2 key.currency is signed in normalized form. A three-character code is kept exactly as given: currency codes are case-sensitive. A 40-character hex code is upper-cased, and when it encodes a three-character code made of letters, digits or the symbols ?!@#$%^&*<>(){}[]| it is signed as that code. Any other name is signed as its 40-character upper-case hex encoding.registry, the delegationManager, and the addresses inside an erc7710_delegation condition) are signed in lower case (Sections 7.1, 7.2).Block anchoring binds an attestation result to a specific point in blockchain history. Without it, a verifier cannot distinguish a result from 10 seconds ago from one from 10 minutes ago.
| Field | Format | Description |
|---|---|---|
blockNumber | 0x-prefixed hex string | The block at which state was read |
blockTimestamp | ISO 8601 datetime | The timestamp of that block |
blockNumber and blockTimestamp are included for every EVM chain.slot for Solana, ledgerIndex with ledgerHash for XRPL and Stellar, blockHeight with blockHash for Bitcoin and Tron, and checkpointSequence for Sui.met: false.error.code "rpc_failure" and error.failedConditions, a list of {source, chainId, message} naming the kind of read that did not complete and the chain (an integer for EVM chains, a chain name otherwise, or null). No signature is returned.met: false that anyone replaying the read at that anchor obtains again.blockNumber and blockTimestamp MUST correspond to the same block.decimals(), an attestation lookup, an identity registry), is made at that block, and a time-dependent test such as an attestation's expiry is judged at that block's own timestamp.ledgerIndex with its ledgerHash names the one validated ledger every read behind the result was pinned to. A Solana slot is a floor: the state was observed at that slot or later, and across a paged read it is the lowest slot any page reported. The Bitcoin and Tron blockHeight with blockHash, the Stellar ledgerIndex with ledgerHash and the Sui checkpointSequence are tip markers captured alongside the read: they bind the observation to a recent point in chain history and do not name the exact state that was read.Verifiers MAY enforce a maximum age by comparing blockTimestamp to their local clock. A verifier SHOULD allow for reasonable clock skew (RECOMMENDED: 60 seconds) and block propagation delay.
For a live access decision, where the result decides something now (admitting a request, pricing a purchase, settling a payment), a verifier that does not choose its own maximum age SHOULD use a default of 120 seconds, before the clock-skew allowance above. The anchor can sit a few blocks behind the tip (Section 9.5, about 72 seconds on Ethereum mainnet in proof mode), so a smaller default would refuse valid results on slower chains; a larger one lets the yes and the action it authorizes drift apart. A relying party whose action is costly to reverse MAY set a tighter value. The maximum age applies to the decision, not to the artifact: a result kept as evidence of what was true at its anchor (Section 13.6) has no maximum age, and only Check 4 bounds its issuance window.
If blockTimestamp is absent, the verifier SHOULD fall back to attestedAt for freshness checks, with the understanding that this provides weaker guarantees.
The anchor is chosen by the API, not by the caller. Standard requests anchor at the chain tip. Requests with proof: "merkle", on /v1/attest, /v1/trust and /v1/trust/batch alike, anchor a few blocks behind the tip: a Merkle proof requires the state trie for the anchored block, and nodes commonly serve a block before its state is retrievable, so anchoring at the tip would intermittently make proofs unavailable. The lag is currently six blocks (about 12 seconds on Base, about 72 seconds on Ethereum mainnet) and applies to the whole request, so the verdict and the proof always describe the same block, as required by Section 9.3.
Verifiers MUST NOT assume the anchor equals the chain tip at the time of the response, and MUST NOT treat a small gap between the anchor and the tip as evidence of a stale or replayed attestation. Freshness is evaluated as specified in Section 9.4, against blockTimestamp and expiresAt, not against the distance from the tip.
When requested via proof: "merkle", InsumerAPI includes a Merkle storage proof alongside the boolean result. On /v1/attest the request may also be made with the query parameter proof=merkle. A proof lets a verifier confirm the on-chain value against a block header without relying on InsumerAPI’s reading of the chain. The response does not include the header: the verifier obtains it for blockNumber from a source it trusts.
Four kinds of proof exist, and each proves a different part of the state.
A token_balance or ratio_to_amount condition on an ERC-20 contract produces a balance proof (both prove the same balance slot) when ALL of the following are true:
available: false.A token_balance or ratio_to_amount condition whose contractAddress is "native" produces an account proof on the same chains. A native balance is a field of the account, not an entry in a contract's storage, so the proof is the account's own branch of the state trie. It carries subject: "account_balance" with the account's balance, nonce and codeHash, and no storageProof, mappingSlot or storageKey. The balance field is the value the condition was evaluated against, and the proof is served only when the two agree. An account proof covers a zero balance as readily as a non-zero one.
An account_code condition produces an account proof of the same shape on the same chains, carrying subject: "account_code". Its codeHash is the value the condition was evaluated against, and the proof is served only when the two agree: keccak256 of empty code (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) means no code; keccak256 of 0xef0100 followed by the 20-byte target means an EIP-7702 delegation to that target, which a verifier can check only with the delegate it supplies; anything else means contract code. An account absent from the state trie may report every field as zero, which also means no code. The record carries the account's balance and nonce as well, as every account proof does.
An erc7710_delegation condition produces a revocation proof (Section 10.7) when the DelegationManager the delegation was signed against is a deployment whose storage layout has been verified on-chain. Currently that is the v1.3.0 manager on Base, 0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3 (signed in lower case, Section 8.3). Other recognized managers return available: false with a reason naming the unverified layout.
No other condition type produces a proof. nft_ownership and erc8004_agent in particular do not, and no proof exists on a non-EVM chain.
When unavailable, the response includes a proof object with available: false and a reason string.
{
"available": true,
"type": "merkle",
"blockNumber": "0x12a05f200",
"mappingSlot": 3,
"storageKey": "0x...",
"accountProof": ["0x...", "0x...", ...],
"storageProof": [
{
"key": "0x...",
"value": "0x3B9ACA00",
"proof": ["0x...", "0x...", ...]
}
],
"storageHash": "0x..."
}| Field | Type | Description |
|---|---|---|
available | boolean | true if proof was generated |
type | "merkle" | Proof type identifier |
subject | string | OPTIONAL. What the proof is about. Omitted on ERC-20 balance proofs; "account_balance" on account proofs for a native balance and "account_code" on account proofs for an account_code condition (Section 10.2); "delegation_revocation" on erc7710_delegation proofs (Section 10.7). It is how the four are distinguished. |
balance | string | OPTIONAL. Hex-quantity native balance proven by the account branch. Present on "account_balance" and "account_code" proofs; on "account_balance" it is the value evaluated |
nonce | string | OPTIONAL. Hex-quantity account nonce from the same branch. Present on "account_balance" and "account_code" proofs |
codeHash | string | OPTIONAL. Account code hash from the same branch. Present on "account_balance" and "account_code" proofs; on "account_code" it is the value evaluated (Section 10.2) |
blockNumber | string | Hex block number at which the proof was generated. MUST match the result's blockNumber. |
contractAddress | string | OPTIONAL. The contract whose storage was proven. Present on erc7710_delegation proofs, where it is the DelegationManager. |
mappingSlot | integer | Storage slot of the proven mapping: the ERC-20 balanceOf mapping on a balance proof, the disabledDelegations mapping on a revocation proof |
storageKey | string | Keccak-256 storage key used for the proof: keccak256(abi.encode(key, uint256(mappingSlot))), where key is the wallet address on a balance proof and delegationHash on a revocation proof |
accountProof | string[] | Merkle-Patricia trie proof nodes from state root to the account: the contract's account on a balance or revocation proof, the wallet's own account on an account proof |
storageProof | object[] | Proof nodes from the contract's storage root to the proven slot |
storageProof[].key | string | Storage key being proven |
storageProof[].value | string | The proven slot value (hex BigInt). Raw balance before decimal division on a balance proof; 1 (revoked) or 0 (not revoked) on a revocation proof |
storageProof[].proof | string[] | Merkle proof nodes |
storageHash | string | Storage root hash of the contract account |
InsumerAPI may include additional fields in the proof object. Verifiers SHOULD ignore fields they do not recognize.
Merkle proofs can be verified against any Ethereum block header source (archive node, light client, block explorer). For custom implementations, the proof structure follows standard Merkle-Patricia trie verification: verify accountProof against the block's state root, then verify storageProof against the account's storage root. For an account proof only the first step applies: the account branch itself yields the evaluated field: the balance, or on an account_code proof the codeHash.
Standard attestations (without proof) return only a boolean and never reveal balances, with one exception: ratio_to_supply on v1 keys (Section 13.3). Merkle proofs do reveal the raw on-chain balance. Callers MUST be aware that requesting proofs trades privacy for trustlessness.
When the proof cannot be generated:
{
"type": "merkle",
"available": false,
"reason": "Merkle proofs not available for this chain"
}An unavailable proof may also carry transient (true when a read did not complete and the same request may succeed later; false when the outcome is settled) and unsupportedChain: true on a chain that serves no proofs. Some variants omit type.
The proof object, available or not and reason included, sits inside the signed results array (and the results claim of a JWT). It is covered by the signature but not by conditionHash, so a verifier MUST NOT remove or rewrite any of its fields before checking the signature. The text of reason is informational and may change between releases.
An erc7710_delegation verdict (Section 7.2) has three parts, and they are not equally checkable by default. The EIP-712 signature verification and the caveat decode are reproducible by the caller without assistance, because the caller holds the delegation object and submitted the raw caveat terms (Section 7.2.1). The remaining clause, “not revoked as of block N”, was an assertion by the API that a verifier could not independently confirm. A revocation proof closes that gap, making the verdict checkable rather than partly trusted.
When a delegation condition is requested with proof: "merkle" and the manager's layout has been verified (Section 10.2), the proof object proves the value of disabledDelegations[delegationHash] in the DelegationManager's storage against the anchored block's state root.
{
"available": true,
"type": "merkle",
"subject": "delegation_revocation",
"blockNumber": "0x12a05f200",
"contractAddress": "0xdb9b1e94b5b69df7e401ddbede43491141047db3",
"mappingSlot": 4,
"storageKey": "0x...",
"accountProof": ["0x...", "0x...", ...],
"storageProof": [
{
"key": "0x...",
"value": "0x0",
"proof": ["0x...", "0x...", ...]
}
],
"storageHash": "0x..."
}A verifier checks a revocation proof as follows:
storageKey = keccak256(abi.encode(delegationHash, mappingSlot)), taking delegationHash from the signed evaluatedCondition, and confirm it equals the returned storageKey. This step is REQUIRED: it is what binds the proof to this specific delegation. Without it, a proof of an unrelated storage slot would still verify correctly against the block header.accountProof against the state root of the block header for blockNumber, then verify storageProof against the account's storage root, as in Section 10.4.1 means the delegation was revoked, 0 means it was not. Note that this proves absence as firmly as presence: a proven 0 is positive proof that no revocation exists at that slot, not merely a report that none was found.Proofs are produced only for DelegationManager deployments whose storage layout has been verified on-chain, and the restriction is deliberate rather than provisional. An inferred slot number would yield a well-formed proof of the wrong storage location, and that proof would verify against the block header exactly as a correct one does. A verifier would have no way to detect the mistake. That outcome is worse than returning no proof, so managers without a verified layout return available: false and the boolean verdict is computed and signed as normal.
Unlike a balance proof (Section 10.5), a revocation proof reveals no balance and no wallet holdings: the proven slot is a single revocation flag keyed by a delegation hash the caller already holds.
A trust profile is a higher-level construct built on top of attestations. It evaluates a curated set of conditions across multiple dimensions to produce a fact-based wallet profile.
{
"id": "TRST-74167",
"wallet": "0x1601...347E",
"conditionSetVersion": "2026-10-08",
"dimensions": {
"stablecoins": { "checks": [...], "passCount": 6, "failCount": 46, "notEvaluatedCount": 0, "total": 52 },
"governance": { "checks": [...], "passCount": 0, "failCount": 8, "notEvaluatedCount": 0, "total": 8 },
"nfts": { "checks": [...], "passCount": 0, "failCount": 3, "notEvaluatedCount": 0, "total": 3 },
"staking": { "checks": [...], "passCount": 0, "failCount": 5, "notEvaluatedCount": 0, "total": 5 },
"institutional_stablecoins": { "checks": [...], "passCount": 0, "failCount": 4, "notEvaluatedCount": 4, "total": 8 },
"tokenized_treasuries": { "checks": [...], "passCount": 0, "failCount": 15, "notEvaluatedCount": 1, "total": 16 },
"stablecoin_deposits": { "checks": [...], "passCount": 5, "failCount": 34, "notEvaluatedCount": 0, "total": 39 },
"wrapped_bitcoin": { "checks": [...], "passCount": 0, "failCount": 12, "notEvaluatedCount": 0, "total": 12 },
"names": { "checks": [...], "passCount": 0, "failCount": 2, "notEvaluatedCount": 0, "total": 2 },
"account": { "checks": [...], "passCount": 5, "failCount": 5, "notEvaluatedCount": 0, "total": 10 },
"solana": { "checks": [...], "passCount": 1, "failCount": 13, "notEvaluatedCount": 0, "total": 14 }
},
"summary": {
"totalChecks": 169,
"totalPassed": 17,
"totalFailed": 147,
"totalNotEvaluated": 5,
"dimensionsWithActivity": 4,
"dimensionsChecked": 11
},
"profiledAt": "2026-10-07T21:53:03.720Z",
"expiresAt": "2026-10-07T22:23:03.720Z"
}Trust profile IDs use the format TRST- followed by 5 uppercase hex characters.
A dimension groups related checks. Each dimension contains:
| Field | Type | Description |
|---|---|---|
checks | array | Array of check objects: label, chainId, met, evaluatedCondition, conditionHash, the chain anchor for that chain family, trustLineState on an XRPL trust-line check, proof in proof mode (Section 11.6), and evaluated/reason/requires on a check that could not be evaluated. Unlike attestation results they carry no condition or type fields. Every dimension also reports notEvaluatedCount, zero when all its checks were evaluated. |
passCount | integer | Checks where met is true |
failCount | integer | Evaluated checks where met is false |
notEvaluatedCount | integer | Checks carrying evaluated: false (see below). passCount + failCount + notEvaluatedCount = total. |
total | integer | Total checks in this dimension |
Every check in the curated set is a presence check: a token balance above zero, an NFT held, or, in the account dimension, code present at the wallet address. A curated check on a chain whose wallet is an optional request parameter (Solana, XRPL, Stellar, Sui) cannot be evaluated when that wallet was not supplied. Such a check stays in the signed profile so the condition set is stable, and carries evaluated: false, reason: "wallet_not_provided", and requires naming the parameter. It carries no anchor, met is false, and it is excluded from passCount and failCount; the summary reports the same total as totalNotEvaluated. Verifiers MUST NOT read an unevaluated check as evidence that the condition was not met. Evaluated checks do not carry the evaluated field.
Ten base dimensions are always present, in this fixed order: stablecoins, governance, nfts, staking, institutional_stablecoins, tokenized_treasuries, stablecoin_deposits, wrapped_bitcoin, names, and account, followed by any of solana, xrpl, bitcoin and tron that were switched on, in that order. The order is the same for every wallet, in a batch as in a single profile. Readers MAY look dimensions up by name; the order of checks inside a dimension is stable. The account dimension holds two checks per chain on Ethereum, Base, Arbitrum, Optimism and Polygon: contract code present (bytecode other than the EIP-7702 designator at the wallet address) and EIP-7702 delegation present (the designator at the address). The two are exclusive on a chain and a plain key reads false on both; which contract is never named. Each row's evaluatedCondition is the Section 7.3 layout, so its conditionHash equals that of the equivalent account_code attest condition. These rows carry available: false in proof mode. Signatures are unaffected: the v2 preimage is canonical JSON with sorted keys, and the v1 preimage is the object exactly as the issuer serialized it, which a verifier checks as received. Four optional dimensions appear only when their wallet parameter is supplied: solana (solanaWallet), xrpl (xrplWallet), bitcoin (bitcoinWallet), and tron (tronWallet). A stellarWallet or suiWallet adds no dimension; it lets the Stellar and Sui checks inside the base dimensions (institutional_stablecoins, and USDY on Sui in tokenized_treasuries) be evaluated.
The conditionSetVersion field is a dated identifier of the curated check list that was run, currently "2026-10-08". It carries the same value on every key version and sits inside the signed payload. It is bumped whenever a row is added to or removed from the set. Readers MAY log it and SHOULD treat profiles carrying different values as incomparable. Signature verification MUST NOT fail solely because the value is unfamiliar: the kid, not this field, selects the signing preimage (Section 3). A relying application MAY require a particular value for its own policy.
The entire trust object (including id, wallet, conditionSetVersion, dimensions, summary, profiledAt, expiresAt) is signed as a single payload using the same ECDSA P-256 mechanism described in Section 3.
POST /v1/trust requires wallet, an EVM address. solanaWallet, xrplWallet, bitcoinWallet, tronWallet, stellarWallet and suiWallet are optional; one that is supplied but malformed is rejected with a 400, never dropped.
With proof: "merkle" each evaluated check carries a proof object (Section 10). Only EVM token checks can carry an available proof: NFT checks, checks on non-EVM chains, the account rows (Section 11.3), and token checks on tokens that compute balances rather than storing them return available: false. A check that was not evaluated carries none. The proof sits inside the signed dimensions, outside conditionHash.
POST /v1/trust/batch takes wallets, an array of 1 to 10 objects, each with the same wallet fields as /v1/trust. The response's data.results holds one entry per wallet, in request order: either {trust, sig, kid, pqSig, pqKid}, a profile signed on its own and verified exactly as a single profile is, or an unsigned {error: {wallet, message}} for a wallet that could not be profiled. data.summary is {requested, succeeded, failed} and is not signed. Every profile in one batch is read at the same block on each EVM chain.
A conforming verifier MUST implement checks 1 and 2. Checks 2a, 3 and 4 are RECOMMENDED; Checks 5 and 6 are OPTIONAL. Checks are numbered in publication order; Check 2a was added after Check 6 and keeps the numbers of the others. A verifier reports six independent verdicts: signature (Check 1), condition hashes (Check 2), counts (Check 2a), freshness (Check 3), expiry (Check 4), and the post-quantum signature (Check 6); Check 5 verifies a proof carried with the artifact and is not one of the six verdicts.
https://api.insumermodel.com/v1/jwks.kid. The set holds keys of two types (Section 4.1); select by kid, never by position, and treat an unknown kid as unverifiable. A kid that selects no key (missing, unknown, or the key set unreachable) fails this check alone: Checks 2 to 4 need no key and are still performed and reported as their own verdicts, and the post-quantum verdict of Check 6 is determined by what was transmitted, absent when no post-quantum signature was and unverifiable when one was, since that kid selects no preimage. The artifact is rejected whenever Check 1 does not pass.kid selects (Section 14):
insumer-attest-v2, insumer-trust-v2): for an attestation "insumer.attestation.v2" + "\n" + canonical_json({"v":2,"id":"...","pass":...,"results":[...],"attestedAt":"..."}); for a trust profile "insumer.trust.v2" + "\n" + canonical_json(trust) where trust is the whole returned trust object (no "v" member, expiresAt included); where canonical_json is the RFC 8785 form with keys sorted lexicographically and recursively (Section 3.3.1).insumer-attest-v1): for an attestation {"id":"...","pass":...,"results":[...],"attestedAt":"..."}; for a trust profile the whole trust object, {"id","wallet","conditionSetVersion","dimensions","summary","profiledAt","expiresAt"}; in each case with fields in that exact insertion order and no domain tag (Section 3.3).SHA-256 of the preimage bytes.The simplest approach is to use insumer-verify, which handles payload reconstruction and verification in a single call; it is published for Node.js on npm and for Python on PyPI under the same name, and both pass the published test vectors.
For each result in results (for a trust profile, each check in every dimensions[*].checks):
canonical_json(evaluatedCondition) (keys sorted lexicographically and recursively, no whitespace, Section 8).SHA-256 of the resulting bytes."0x" + hex(digest) to the claimed conditionHash.passCount and failCount are not signed (Section 3.3). Count the results whose met is true and the results whose met is false, and compare them with passCount and failCount. If either differs, or only one of the two is present, REJECT the attestation: a count was changed after signing. An attestation carrying neither count passes this check. A JWT carries no counts, so the check does not apply to it. The reference verifier reports this check as its own verdict, checks.counts, since insumer-verify 1.11.0.
If blockTimestamp is present:
blockTimestamp as a UTC datetime.age = now() - blockTimestamp.age exceeds the verifier's configured maxAge (RECOMMENDED default for a live access decision: 120 seconds, Section 9.4; no maximum when the result is read as evidence), REJECT or flag the attestation as stale.If blockTimestamp is absent, the verifier MAY use attestedAt as a weaker freshness signal. For a trust profile, profiledAt takes the place of attestedAt.
expiresAt as a UTC datetime.expiresAt is later than attestedAt plus the issuance window (30 minutes, or 5 minutes when any result carries an erc7710_delegation condition), REJECT the attestation as tampered.now() > expiresAt, REJECT the attestation as expired.expiresAt is not part of the signed payload (Section 3.3), while attestedAt is. InsumerAPI never issues a validity window longer than the values above, so an expiresAt further from the signed attestedAt than the issuance window can only have been edited after signing; step 2 rejects it without waiting for the expiry step, which would otherwise be reading an attacker-chosen clock. An honest attestation is unaffected: its expiresAt is exactly attestedAt plus the window. The insumer-verify library performs this binding automatically. The step does not apply on the JWT path (Section 5.4), where exp is inside the signed token. Verifiers MAY tolerate up to 60 seconds of skew on this binding, rejecting only when expiresAt exceeds attestedAt plus the window by more than that; the reference verifier applies that 60 second tolerance. Verifiers MAY likewise apply a small clock-skew allowance (RECOMMENDED: 60 seconds, as in Section 9.4) to the age computed in Check 3 and to step 3 above; the rule stays fail-closed, and an attestation older than expiresAt plus the allowance is rejected. The reference verifier applies this allowance from release 1.8.4 (option clockSkew, default 60 seconds, 0 disables it).
If a Merkle proof is present and available is true, verify it against a trusted block header source. If subject is "delegation_revocation", the verifier MUST also recompute storageKey from the delegationHash in evaluatedCondition before relying on the proven value, as specified in Section 10.7.
When a response carries pqSig and pqKid (raw format) or a sibling pqJwt (JWT format), the verifier MAY check the post-quantum signature and MUST report the result as its own verdict, separate from Checks 1 to 4. The post-quantum signature is additive: sig, kid, and jwt are unchanged, and a verifier that does not implement this check ignores the fields.
pqKid in the JWKS to an RFC 9964 key (kty "AKP", alg "ML-DSA-65", raw public key in pub). An unresolvable pqKid is unverifiable, never a pass.kid), prepend the domain tag: "insumer.attestation.pq1" + "\n" + preimage (trust profiles: "insumer.trust.pq1"), and verify the ML-DSA-65 signature in pqSig (base64) over those UTF-8 bytes (pure ML-DSA, empty context). A classical kid the verifier does not recognise selects no preimage, so the post-quantum signature cannot be evaluated: it is unverifiable, never refuted. A verifier MUST NOT fall back to another era's preimage for an unrecognised kid, because the post-quantum signature would then be checked against bytes the issuer never signed and a genuine one reported as refuted the first time a new signing kid appears. A recognised classical kid that names the other artifact type is a relabelled artifact, not a new era, and is refuted as before (test vector 21); a pqKid that names the other artifact type is unverifiable (test vector 22).pqJwt: verify the compact JWS (header alg "ML-DSA-65", kid = the PQ kid) over its own header.payload bytes, then confirm that its payload and the ES256 JWT's payload carry the identical claim set: the same member names, and for every member a deeply equal JSON value (objects compared without regard to member order, arrays in order, numbers by value). Any difference, whether a changed value, a missing member or an extra member on either side, makes the post-quantum signature refuted. The post-quantum signature therefore vouches for every claim a relying party reads from the ES256 JWT, sub, results, conditionHash and the block anchor included, and cannot be transplanted from another artifact. Byte-identical payload segments satisfy this trivially; a verifier MUST NOT require byte identity, since two serializers may order members differently. Test vectors 24 and 25 are the refused cases.format: "jwt" response, verified wholeWith format: "jwt" the response carries the tokens beside the attestation, in one object: attestation, sig, kid, pqSig, pqKid, and jwt, pqJwt. The signed attestation does not in general name the wallet (only erc8004_agent and erc7710_delegation results carry it, in evaluatedCondition); the jwt does, in sub. A verifier handed the whole object therefore SHOULD verify the tokens as well as the attestation, and report them as their own verdict: the ES256 signature of jwt under the same kid as the response, its condition hashes (Check 2), that it is the token of this attestation (jti equals id, and pass, results and exp equal the attestation's pass, results and expiresAt in whole seconds), and the pqJwt post-quantum signature as in Check 6. A failure fails the artifact. A relying party that reads claims from a jwt MUST have verified that jwt; verifying the attestation beside it is not a substitute. Test vectors 26 and 27 are the two cases; insumer-verify reports the verdict as checks.jwt from 1.8.7.
Without Merkle proofs, the verifier trusts InsumerAPI to have read chain state honestly. The signature guarantees InsumerAPI produced the attestation (non-repudiation) and that it has not been modified in transit (integrity). Merkle proofs provide an additional layer: they allow the verifier to independently confirm the on-chain value against a block header obtained from a source the verifier trusts, without relying on InsumerAPI’s reading of the chain.
Attestation IDs and attestedAt timestamps provide replay detection. Verifiers SHOULD reject attestations they have seen before (by ID) or that exceed their acceptable age.
Standard attestations reveal only a boolean (met: true/false) and never expose raw balances or holdings quantities. This is a deliberate privacy property. The one exception is ratio_to_supply on v1 keys: its result also carries totalSupply and the computed fraction, from which the balance can be derived. Under v2 no fraction is returned.
Merkle proofs reveal the raw balance. Callers requesting proofs MUST understand this tradeoff.
InsumerAPI does not log or store wallet addresses beyond the request-response cycle. See the Privacy Policy for details.
The condition hash chain (Section 8) ensures that modifying any aspect of the evaluated condition (the contract address, threshold, comparison, chain, or, under v1, decimals) invalidates the signature. This prevents an intermediary from substituting conditions after signing.
Verifiers SHOULD enforce freshness bounds (Check 3) to ensure attestations reflect recent chain state.
This section describes /v1/attest; the differences on /v1/trust are at the end. A signed result is InsumerAPI's statement that it read chain state, evaluated the conditions in evaluatedCondition, and got the met and pass values shown, at the time in attestedAt. The signature covers id, pass, results and attestedAt together, so neither the conditions nor the outcome can be changed afterwards. What it does not show:
proof: "merkle", for token balance, ratio-to-amount, account code and delegation revocation conditions on EVM chains, where available), the proven value can be checked against the chain's own state root. The response does not include the block header: fetch it for blockNumber from a node you trust. A balance proof exposes the raw balance to anyone holding the result; an account-code proof carries the account record, native balance included; a revocation proof exposes no balance. Without a proof, the result is as reliable as InsumerAPI, its upstream data sources, and its signing keys.format: "jwt" and match sub against the wallet you expect, comparing EVM addresses without regard to letter case. sub names one wallet: if a request covers more than one chain family, send one family per request.eas_attestation result says only that a third party's attestation exists for the wallet.expiresAt. A wallet can change inside that window. A new call reflects the change.blockNumber, or ledgerIndex and ledgerHash), so anyone with the inputs and a node holding that history can repeat the read. A Solana slot is a floor. Bitcoin, Tron, Stellar and Sui anchors mark the chain tip at the time of the result.label is true. A label is signed, but the caller writes it. Compare evaluatedCondition or conditionHash with the conditions your route requires.erc7710_delegation, declaredLimits is a signed decode of what the caller submitted. It is not part of met.expiresAt, passCount and failCount are not signed in the raw format. Derive counts from results; expiresAt is bound to attestedAt by the verifier.What false means. A failed read is refused with a 503 and never signed (on /v1/trust/batch, as an unsigned error entry for that wallet). met: false means the evaluation completed and the condition was not met. Usually the chain answered no. On EVM chains that includes a token or view contract that reverts or has no code. On XRPL a trust line with a freeze set on it, or whose issuer has frozen everything it issued, is false whatever the balance. For erc7710_delegation it can also mean the delegation failed a check on its own contents, or carries a caveat the API does not recognise. For account_code it means the address is in a code state other than expect, or, with delegate, that its EIP-7702 designator points elsewhere. A kid your verifier does not recognise leaves the result unverified: treat it as no result, not as a signed false.
On /v1/trust. The signature covers the whole trust object, including wallet (the EVM wallet), summary and expiresAt, and the time is profiledAt. Other-chain wallets are not in the signed object, and there is no JWT format. A check marked evaluated: false was not run: its wallet was not supplied.
New condition types, proof types, and trust dimensions may be introduced in future versions of this specification. Verifiers SHOULD handle unrecognized condition types gracefully: the condition hash is recomputed over whatever the evaluatedCondition carries, type included, so Check 2 applies to every result; what a verifier cannot do for a type it does not know is interpret the condition, and it SHOULD report that rather than reject the whole attestation. Version changes will be reflected in the meta.version field of API responses.
The signature scheme is versioned independently of meta.version and selected by the response kid. v1 (insumer-attest-v1) signs the bare insertion-order payload (Section 3.3) and carries threshold as a JSON number with a decimals field. v2 (insumer-attest-v2, insumer-trust-v2) signs the domain-separated canonical preimage (Section 3.3.1) and carries quantities as canonical decimal strings. Both verify against the same key published at the JWKS endpoint. All keys created on or after the v2 rollout are v2; keys issued earlier remain v1 and stay verifiable unchanged. Verifiers MUST branch on kid and MUST NOT assume a single scheme. One shape is common to both schemes: the curated checks inside a trust profile (Section 11) are presence checks whose evaluatedCondition carries operator "gt", threshold as the JSON number 0, and, on token checks, decimals; that shape is frozen and is not a v1 marker.
Version 1.0 of this specification has been extended additively since publication. Every change below adds capability or corrects a statement; none alters or invalidates a previously specified signed format. Attestations signed under any earlier revision remain valid and verify unchanged.
| Date | Addition |
|---|---|
| 2026-10-28 | Section 3.3 states that every attestation field outside the signed payload is unsigned, naming passCount, failCount and expiresAt, and Section 5.1 marks the two counts as such. New Check 2a (RECOMMENDED): a verifier recounts the counts from the signed results and rejects a mismatch. The existing checks keep their numbers. Nothing signed changes. |
| 2026-10-21 | Section 9.4 and Check 3 give the freshness check a RECOMMENDED default: a verifier making a live access decision that sets no maximum age of its own uses 120 seconds before the clock-skew allowance, sized to the proof-mode anchor lag of Section 9.5; a result read as evidence has no maximum age. Editorial: no signed format changes. |
| 2026-10-07 | Condition type account_code (Section 6.8): the code state of the wallet address itself at the anchored block, expect "none", "eip7702" or "contract", with an optional delegate the EIP-7702 designator must point at; operator code_state; Section 7.3 gives its evaluatedCondition layout, which echoes nothing read from the chain. Section 10 adds the account proof for this type (subject: "account_code", codeHash as the value evaluated). Section 11.3 adds the tenth base dimension, account (contract code present and EIP-7702 delegation present on five chains), and states that dimensions now come in a fixed order, the base ten first and then the optional four, the same for every wallet; conditionSetVersion is "2026-10-08". No change to any signed format or preimage; profiles and attestations signed under earlier revisions verify unchanged. |
| 2026-10-06 | Corrections and additions from a line-by-line check against the issuer. Corrected: the proof object, reason included, is inside the signed results (Section 10.6), not outside every signed payload; the erc8004_agent registry and the DelegationManager are signed in lower case, and the examples in Sections 7.1, 7.2 and 10.7 now show them that way; jwt is a member of data, and a v1 key's JWT carries insumer-attest-v1 (Section 5.4); the decimals cross-check runs on EVM chains, Tron and Sui (Section 6.1); decoder is a contract address and decoderMethod is listed (Section 7); a time-window caveat is judged at the anchored block's timestamp (Section 7.2.1); the v1 ratio threshold has no ceiling (Section 6.6); a zero ERC-20 balance can be proven once its slot is located (Section 10.2); the proof-mode anchor lag is six blocks and applies to trust requests too (Section 9.5); curated NFT checks carry no decimals (Section 14.1). Added: token_balance and nft_ownership on every chain family, with currency, assetCode and taxon (Sections 6.1, 6.2); indexer and the chains for raw EAS conditions (Section 6.3); input limits (Section 6.7); how identifiers are spelled in the signed condition (Section 8.3); the 503 refusal body (Section 9.3); further unavailable-proof fields (Section 10.6); label defaults and NFC normalization (Section 5.2); trust requests, proof mode and the batch response (Section 11.6); the canonical decimal-string form (Section 8.3); the default labels, issuer-wide freeze and type-specific result fields (Section 5.2); farcaster_id always read on chain 10 (Section 6.4); the EAS schema check and expiry at the anchored timestamp (Section 6.3); undecodable caveat terms counted as unrecognized (Section 7.2); multiple, amount, minFraction, assetCode and gte_fraction in the Section 7 table; the v1 trust preimage in Check 1 and trust profiles in Checks 2 and 3; the ratio_to_supply exception to the privacy statements on v1 keys (Sections 10.5, 13.3). Also corrected: Appendix A names the v2 preimage; Sections 10.1 and 13.1 say a Merkle proof removes reliance on InsumerAPI’s reading, with the header from a source the verifier trusts; Section 8.1 makes the condition-hash check a MUST, as Section 12 already did. No format changes: the claim set, both signatures and every preimage are unchanged. |
| 2026-10-05 | Editorial precision: Section 11.4 states that signature verification must not fail solely because conditionSetVersion is unfamiliar, and that a relying application may require a particular value for its own policy; it replaces the earlier wording that readers must not reject a profile on this field. The evaluatedCondition field table states that currency and taxon appear on any XRPL condition that supplied them. |
| 2026-10-04 | Section 13.6 states what a signed result shows and what it does not: how far the issuer must be trusted, whose wallet was read, that control of the wallet and the person's identity are not checked, the anchor each chain carries, which fields sit outside the signature, and what met: false means beside a refused read. It states that a failed read is refused and never signed, and that on XRPL a trust line is false when it is frozen, or when its issuer has frozen everything it issued. No format changes: the claim set, both signatures and every preimage are unchanged. |
| 2026-10-01 | Section 11.3 names nine base dimensions (adding tokenized_treasuries, stablecoin_deposits, wrapped_bitcoin and names) and states that every curated check is a presence check. Section 11.4 restates conditionSetVersion as a dated identifier of the check list ("2026-10"), the same on every key version, that readers log and never reject on; the kid alone selects the preimage. The example values in Sections 3.3.1 and 11.1 are updated to match. No format changes: the trust object layout, both signatures and every preimage are unchanged, and profiles signed under the earlier identifier verify as before. |
| 2026-09-27 | Editorial precision: Appendix A names the key sort order as UTF-16 code units (RFC 8785); Section 14 states that the condition hash is recomputed for every condition type, known or not; Check 6 names the classical kid in the relabelled-artifact rule and the pqKid in the unverifiable rule. No format changes. |
| 2026-09-27 | Section 12, Check 1 states that a kid which selects no key fails the signature verdict alone: Checks 2 to 4 are still performed and reported as their own verdicts, and a companion that was not transmitted is absent. The reference verifier reports this way from insumer-verify 1.9.2; test vectors 11 and 19 restated to match. Verifier guidance; nothing signed changes. |
| 2026-09-25 | Section 4.2 states that keys are never removed from the JWKS document: a rotated key stays permanently under its original kid. This replaces the earlier statement that a previous key would remain until outstanding attestations had expired. Section 4.2 also recommends that relying parties keeping attestations as records keep the JWKS and key-binding documents with them. |
| 2026-09-24 | Section 5.4 states what sub names: the wallet a condition in the request evaluated. A wallet field no condition reads is never named, and for conditions spanning chain families sub names the first evaluated family. The issuer enforces this from the same date. No format changes: the claim set, both signatures and every preimage are unchanged. |
| 2026-09-22 | Section 12, Check 6 states how a verifier treats a classical kid it does not recognise: that kid selects no preimage, so the companion is unverifiable rather than refuted, and no other era’s preimage may be substituted for it. A recognised kid naming the other artifact type is a relabelled artifact and is refuted as before (vector 21). Verifier guidance; nothing signed changes. |
| 2026-09-20 | Section 6.1 states that a token's scale is always its own: decimals in a request is a cross-check that must agree with the token, and a value that differs is refused. Section 8.2's example carries the threshold in display units, as conditions are written and signed ("1000" is 1000 USDC), with the condition hash of those bytes. Section 7's field table states that decimals appears in an evaluated condition under v1 only, as Section 8.2 already described. Section 10 describes all three kinds of proof: ERC-20 balance proofs (token_balance and ratio_to_amount), account proofs for a native balance (subject: "account_balance"), and delegation revocation proofs. The Appendix A example is the Section 8.2 object. No signed format changes. |
| 2026-09-20 | Section 9.3 states what each anchor is: on EVM every read behind a result, supporting reads included, is made at the anchored block and time-dependent tests use that block's timestamp; an XRPL ledger names the pinned state; a Solana slot is a floor; the Bitcoin, Tron, Stellar and Sui anchors are tip markers. No format changes. |
| 2026-09-20 | Section 9.3 states the refusal rule for every chain family: a result is signed only from a complete, anchored observation, a failed or partial read returns 503 and is never reported as met: false, and what the chain itself answers (a revert, an address with no code, an account absent at an anchored ledger) is an observation and is signed. No format changes. |
| 2026-09-20 | Section 12 closes with how a verifier treats a whole format: "jwt" response: the tokens beside the attestation are verified and bound to it, and reported as their own verdict. Test vectors 26 and 27 added; implemented in insumer-verify 1.8.7. |
| 2026-09-20 | Section 12, Check 6: in the JWT format the companion now binds the full claim set. A pqJwt is confirmed against its jwt by every claim, compared as parsed JSON and never by bytes, where the check previously named jti, exp and pass. Nothing the issuer signs changes and every issued artifact verifies as before; test vectors 24 and 25 added; implemented in insumer-verify 1.8.6. |
| 2026-09-02 | Section 12 states how the six checks map to the five verdicts and the clock-skew tolerances a verifier may apply (Checks 3 and 4, and the expiresAt binding); Section 7.2 notes that the caveat counts are decimal strings. |
| 2026-09-02 | Trust-profile examples and prose aligned with live responses (Sections 11.1 and 11.3: five base dimensions, the check-object field list, notEvaluatedCount and totalNotEvaluated in the example); threshold typed per scheme in Sections 6.1 and 7; delegation caveat counts shown as the decimal strings they are; Check 5 now precedes Check 6. |
| 2026-09-02 | Editorial precision: the v2 trust preimage is stated exactly (Sections 3.3.1 and 12, Check 1: the whole trust object under insumer.trust.v2, no "v" member); Section 4.1 lists the two AKP entries beside the three EC kids and the select-by-kid rule; Section 6.6 summarizes the ratio conditions; new Section 14.3 gives the key-binding verification procedure; Appendix B no longer counts the checks. |
| 2026-09-02 | Unevaluated trust-profile checks marked (Section 11.3): a curated check whose chain wallet was not supplied stays in the signed profile with evaluated: false, reason and requires, carries no anchor, and is counted in notEvaluatedCount / totalNotEvaluated instead of pass or fail. Signing is unchanged for both key eras; the marker is content the existing schemes sign. |
| 2026-09-01 | Post-quantum companion signatures (Section 12, Check 6): responses gain optional siblings pqSig/pqKid (raw format) and pqJwt (JWT format), ML-DSA-65 (FIPS 204) over the classical preimage under a post-quantum domain tag; the JWKS gains RFC 9964 AKP entries appended after the EC entries. Every existing field, signature, preimage, and key is unchanged; verifiers that do not implement Check 6 are unaffected. |
| 2026-09-01 | Expiry verification hardened (Check 4, Section 12): a step binding the unsigned expiresAt to the signed attestedAt via the issuance window, so a verifier rejects a moved expiry as tampered instead of honoring it. Documents the check insumer-verify already performs; the attestation format, signed payload, and issuance behavior are unchanged. The expiresAt field row (Section 5.1) now also names the 5-minute delegation window alongside the 30-minute default. |
| 2026-07-28 | evm_view_call specified (Section 6.5): request parameters, calldata construction, the view_call_true operator, signed-false semantics for reverts, and the reproduction procedure. The type itself has been live and additive; this documents it at specification depth alongside the other condition types. |
| 2026-07-28 | Additional failReason value delegator_not_deployed for erc7710_delegation (Section 7.2): reported when the principal address holds no contract code at the anchored block, the state a smart-contract wallet is in until its first transaction. Until this revision such a principal was reported as invalid_signature; the new value names what was evaluated more precisely. Verdicts are unchanged: the condition still fails, and no other failReason changes meaning. |
| 2026-07-27 | Revocation proofs for erc7710_delegation (Section 10.7): with proof: "merkle", an EIP-1186 storage proof of disabledDelegations[delegationHash] against the anchored block's state root, carrying subject: "delegation_revocation" and contractAddress. Returned for DelegationManagers whose storage layout has been verified on-chain. |
| 2026-07-27 | Anchor selection documented (Section 9.5): proof-mode attestations may anchor slightly behind the chain tip so the state trie is retrievable, and verifiers are instructed not to infer staleness from the distance to the tip. |
| 2026-07-27 | Agent standing condition types erc8004_agent and erc7710_delegation (Sections 7.1, 7.2), with the owner_or_bound_wallet and authorized_by_principal operators. Delegation attestations carry a 5-minute expiry. |
| 2026-07-27 | Declared-limit semantics for erc7710_delegation (Sections 7.2.1, 7.2.2): each decoded limit carries the raw terms bytes it was decoded from, and the optional declaredLimits: "omit" request modifier withholds decoded limits from forwarded attestations while leaving the verdict, delegationHash and conditionHash byte-identical. |
| 2026-06-10 | v2 signature scheme (Sections 3.3.1, 14.1): domain-separated canonical preimage, quantities as decimal strings, distinct kid values for attestations and trust profiles. v1 keys and their signatures are unaffected. |
| 2026-06-01 | Ratio condition types ratio_to_amount and ratio_to_supply: dimensionless rules that need no re-tuning across chains, token prices, or transaction sizes. |
| 2026-05-12 | Condition type evm_view_call for arbitrary boolean view functions on EVM chains. |
| 2026-02-25 | EIP-1186 Merkle storage proofs (Section 9), enabling trustless verification against block headers without re-querying a chain. |
The document at /.well-known/pq-key-binding.json binds the ECDSA key to the post-quantum key. It carries a statement (schema insumer.pq_key_binding.v1, issued_at, the classical JWK with its three kids, the post-quantum JWK with its two kids), a contentAddress, two signatures, and an on-chain anchor. A verifier checks it as follows:
contentAddress = "0x" + SHA-256(canonical_json(statement)), with canonical_json exactly as in Section 3.3.1.pqSelfSig is ML-DSA-65 over the UTF-8 bytes of "insumer.pq_key_binding.v1" + "\n" + contentAddress, verified with the post-quantum public key in the statement (the key owns the statement).classicalSig is ES256 (P1363) over the UTF-8 bytes of "insumer.pq_key_binding.v1.classical" + "\n" + contentAddress, verified with the ECDSA public key in the statement (the classical key endorses it).anchor.digest = SHA-256(canonical_json(document without the anchor member)), published as the calldata of anchor.txHash on Base (chain id 8453) in block anchor.blockNumber. Read the transaction from any Base node and compare. The block time is the public record of when the post-quantum key came into existence; it is the value a verifier reading an artifact as evidence uses to say whether a post-quantum signature could have existed when the artifact was issued.AKP key type)This specification uses two different JSON serialization approaches:
Condition hashes (Section 8) use sorted-key canonical JSON:
v1 signature payloads (Section 3.3) use fixed insertion-order JSON: fields appear in the order specified in Section 3.3, not alphabetically. This is the standard output of JSON.stringify() with no replacer argument. v2 signature payloads (Section 3.3.1) are a domain tag and a newline followed by the sorted-key canonical JSON described above.
Condition hash example:
Input evaluatedCondition (unsorted):
{"threshold": "1000", "type": "token_balance", "operator": "gte", "chainId": 1, "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}Sorted for hashing:
{"chainId":1,"contractAddress":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","operator":"gte","threshold":"1000","type":"token_balance"}insumer-verify is the official verification library for InsumerAPI attestations, published for Node.js on npm and for Python on PyPI under the same name. The npm package has zero required dependencies; install the optional peer @noble/post-quantum to verify the post-quantum signature. The Python package requires cryptography; install insumer-verify[pq] for the post-quantum signature. Runs all verification checks described in Section 12 in a single function call:
import { verifyAttestation } from "insumer-verify";
// Pass the full API response envelope, not response.data
const response = await res.json();
const result = await verifyAttestation(response, {
jwksUrl: "https://api.insumermodel.com/v1/jwks"
});
console.log(result.valid); // true - all checks passedAs of v1.3.0, insumer-verify auto-detects the input type: pass a JWT string and it verifies the ES256 signature via JWKS plus the same checks (Section 12), except that a bare token cannot carry its post-quantum signature: supply that separately, or Check 6 reports it absent. Pass an attestation object and it uses the raw verification path.
InsumerAPI is in production and ready to integrate. To start verifying wallets:
POST /v1/attest with a wallet and conditions (add "format": "jwt" for gateway integration)insumer-verify (npm)| Resource | URL |
|---|---|
| API base | https://api.insumermodel.com |
| OpenAPI spec | insumermodel.com/openapi.yaml |
| JWKS | https://api.insumermodel.com/v1/jwks |
| Verification library | insumer-verify (npm, source) |
| MCP server | mcp-server-insumer (npm) |
| LangChain toolkit | langchain-insumer (PyPI) |
| Full documentation | insumermodel.com/developers |