Every condition type InsumerAPI evaluates, what comes back in the signed bytes, what a verifier recomputes, and why each design choice was made. Longer than usual, because the surface has grown.
The primitive has not changed: read wallet state, evaluate conditions, return a signed boolean. What has changed is the range of conditions, the two output formats, the proofs you can attach, and, since the first of September, a second signature beside the first. This is the reference walk-through I would want if I were integrating today. Requests and responses are shown in the shape the API emits; every number here is checked against the running system.
The request shape, and the one rule people trip on
One call, POST /v1/attest, one wallet, one to ten conditions, each with its own chain. The response is a signed attestation: an id, an aggregate pass, per-condition results, and timestamps, with the signature and key identifier beside it.
POST https://api.insumermodel.com/v1/attest
X-API-Key: insr_live_...
{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"conditions": [
{ "type": "token_balance", "chainId": 1,
"contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"threshold": "1000" }
]
}
The rule: quantities are decimal strings. "threshold": "1000", never 1000. Every key issued since June signs a canonical, domain-separated payload in which a string quantity is exact to any precision and serializes identically in every runtime; a JSON number is rejected with a 400. The same holds for the ratio fields below. The one deliberate exception is nft_ownership, whose threshold is the number zero, because it is a presence check and always has been.
What is inside the signed bytes
Each result carries the exact predicate that was evaluated, as evaluatedCondition, and a conditionHash, the SHA-256 of its canonical form. It also carries the chain anchor: blockNumber and blockTimestamp on EVM chains, a ledger index and hash on XRPL and Stellar, a block height and hash on Bitcoin and Tron, a slot on Solana, a checkpoint on Sui. The anchor shape differs by family on purpose, and a verifier that assumes blockNumber finds no anchor on a Bitcoin result. All of it is inside the signature. The expiresAt field is beside the signed payload, not inside it, which is why the spec's verification procedure binds it to the signed attestedAt (an expiresAt further than the issuance window from attestedAt can only have been edited after signing).
That combination, predicate plus hash plus anchor, is the recompute basis. It is what lets anyone re-run the read against any node and compare, which is the property everything else in this guide depends on.
The nine condition types
1. token_balance
Does the wallet hold at least the threshold of a token? Thirty-eight chains: thirty-two EVM plus Solana, XRPL, Bitcoin, Tron, Stellar and Sui. Use "contractAddress": "native" for the chain's own asset. On XRPL the same type covers issued currencies and trust lines, and the result surfaces whether the line is frozen. Why it exists in this shape: the balance itself is never returned, only the boolean, and the threshold travels as a string so the verdict is reproducible to the last decimal.
2. nft_ownership
Does the wallet hold at least one of a collection? Thirty-four chains: the EVM set plus Solana and XRPL. Bitcoin, Tron, Stellar and Sui are balance-only. Why: presence is the question, so the threshold is fixed at zero and the verdict is the cleanest possible boolean.
3. eas_attestation
Does an attestation exist for this wallet on the Ethereum Attestation Service, on any of six chains, matching a schema? Five templates cover the common cases: coinbase_verified_account, coinbase_verified_country, coinbase_one, gitcoin_passport_score and gitcoin_passport_active; any raw schemaId works too. Why: it lets a condition say "cleared a regulated exchange's checks" or "cleared a humanity score" without the caller ever handling a document. The attestation was issued elsewhere; the verdict says it exists.
4. farcaster_id
Does this address own a Farcaster account? Read from the identity registry on Optimism. Why: it is the one social identity that lives on-chain and can therefore be a condition rather than an API call to a platform.
5. evm_view_call
Call any view function on a supported EVM chain that takes a single address and returns a boolean, named by a canonical selector of the form functionName(address). The result is met when the function returns true; a revert is a signed false, not an error. Why: every contract that already encodes membership, allowlisting, delegation or voting rights becomes a gate with no new deployment, and the reproduction procedure is stated in the spec so anyone can re-issue the call.
6. ratio_to_amount
Met when balance is at least multiple times amount, both decimal strings. The derived threshold, computed with a ceiling so the gate errs toward strictness, travels inside evaluatedCondition. Why: "hold ten times what you are about to spend" is one rule that needs no retuning across chains, prices or transaction sizes. EVM chains with direct RPC access.
7. ratio_to_supply
Met when balance divided by totalSupply() is at least minFraction, a decimal string in the open-closed interval up to 1. The raw supply read is a sibling of evaluatedCondition, inside the signed payload but outside the condition hash, so a verifier re-reads balance and supply at the pinned block and re-derives the boolean exactly. Why: share of supply is the honest measure of stake for governance and project tokens; it is meaningless for stablecoins, and we say so.
8. erc8004_agent
Is this wallet the owner of, or the signature-bound agent wallet for, agent agentId in the ERC-8004 identity registry on Base? The result carries agentExists and matchedVia (owner, agent_wallet or none). Why the scope note is in the signed bytes: registration is permissionless minting. The verdict states registration and binding, and implies no vetting, no reputation and no endorsement. Merkle proofs are not offered for this type.
9. erc7710_delegation
Given a delegation object the caller already holds, is it currently valid for this wallet as delegate? Met when: the EIP-712 signature verifies (an EOA signature or an ERC-1271 smart-contract assertion, and the result names which, because those are different claims), the declared delegator matches expectedDelegator, the delegation is not revoked as of the anchored block, every caveat uses a recognized enforcer, and any time-window caveat is open. Managers must be a recognized delegation framework deployment on Base; root authority only; up to sixteen caveats; up to three delegation conditions per request.
Two design points worth understanding. First, declaredLimits: the decoded caveat terms (timestamps, ERC-20 and native spend ceilings, allowed targets, call counts) are reported, not simulated. They sit inside the signed results but outside conditionHash, as a sibling of evaluatedCondition, because the caller submitted those bytes itself and can re-decode them with no external lookup; on-chain redemption enforces them. Second, failReason distinguishes delegate_mismatch, principal_mismatch, invalid_signature, delegator_not_deployed, revoked, unknown_caveat_enforcer and outside_time_window, and delegator_not_deployed is its own reason because no signature was evaluated when the principal has no code on chain yet. Delegation verdicts expire in five minutes, not thirty, because revocation is one transaction away. If the attestation will be forwarded, "declaredLimits": "omit" keeps the principal's ceiling out of it; met and the hashes are byte-identical either way.
Proof mode
Add "proof": "merkle" and a balance condition carries an EIP-1186 storage proof of the balance slot; a delegation condition carries a proof of the revocation slot, tagged subject: "delegation_revocation" with the contract address, mapping slot and storage key alongside the standard account and storage proofs. Verify it against the block header's state root from any header source. For revocation: recompute storageKey = keccak256(abi.encode(delegationHash, mappingSlot)) from the delegationHash in the signed evaluatedCondition first, because that is what binds the proof to this delegation, then read the proven value, where 1 is revoked and 0 is a positive proof that no revocation exists. Proof mode anchors a few blocks behind the tip on purpose, since the state trie is not reliably retrievable at the tip, and the verdict and the proof always describe the same block. It costs two credits instead of one, and on a balance condition it reveals the raw balance, so it trades privacy for trustlessness by explicit choice. If a proof cannot be produced for a transient reason, the premium is refunded and the call costs one.
JWT format
Add "format": "jwt" and the response also carries a standard ES256 JWT with the same verdict, verifiable by any JWT library against the published JWKS with no SDK. The claims: pass, the condition hashes, the block anchor, the results, issuer, subject (the wallet), jti (the attestation id), iat and exp. Why both formats: the raw form is the canonical artifact with the exact signed bytes; the JWT is what an API gateway, a WordPress plugin or an edge worker can check with tooling it already has.
Fact profiles
POST /v1/trust runs a curated set against one wallet: forty-four base checks across twenty-five chains in five dimensions (stablecoins, governance tokens, NFTs, staking, institutional stablecoins), up to forty-nine checks across twenty-seven chains in nine dimensions with the optional Solana, XRPL, Bitcoin and Tron wallets. The whole profile is signed as one object. No score, no opinion, just cryptographically verifiable evidence organized by dimension. POST /v1/trust/batch profiles up to ten wallets in one call, fetching block numbers once, with each profile independently signed. Three credits per profile, six with proofs.
Paying without a key
Every attest and trust call is also reachable with no API key at all via x402 on Base, paid in USDC per call: five cents for an attestation, ten with a proof, fifteen for a profile, thirty with proofs. The invariant, stated exactly: the payer is charged only for a successful answer, and the payer sees the answer only after the payment settled. Evaluation failure means no payment; settlement failure means no response; settlement success means payment, response and the settlement transaction hash in-band so the payment itself is independently checkable. For keyed access there are three paths: POST /v1/keys/create for the email free tier (10 free verifications plus 100 reads/day), POST /v1/keys/buy on-chain, and POST /v1/credits/buy to top up. Prepaid starts at four cents a call.
Verification: five independent verdicts
The reference verifier is insumer-verify on npm, zero required dependencies, Web Crypto only, and it never collapses its answer into one boolean. It reports five verdicts and lets you decide what each means for you:
- Signature. Resolve the key by the
kidon the response from the JWKS; an unresolvablekidfails closed rather than falling back to the first key. Rebuild the exact preimage thekidselects and verify ECDSA P-256 over it. - Condition hashes. Canonicalize each
evaluatedCondition, hash it, compare. - Freshness. Compare the signed
blockTimestampto your own maximum age. The window is yours to set: a checkout wants seconds, an auditor wants none. - Expiry. Bound
expiresAtto the signedattestedAtplus the issuance window, then compare to now. - Post-quantum companion. New, and covered next.
The point of separating them is that "valid" is a policy, and the policy belongs to the relying party. An attestation records that a wallet met a condition at a named block, and that does not decay. The expiry is a freshness policy for access decisions, not a statement about whether the verdict was correct.
The post-quantum companion
Since the first of September, every attest and trust response carries a second signature beside the first: pqSig and pqKid on the raw form, and a sibling pqJwt on the JWT form. The algorithm is ML-DSA-65 under FIPS 204. The public key is published in the same JWKS as RFC 9964 AKP entries under the kids insumer-attest-pq1 and insumer-trust-pq1, appended after the three EC entries so that any verifier still falling back to the first key keeps a classical one.
The design is additive, and that word is doing work. sig, kid, jwt, every preimage and every existing key are byte-for-byte unchanged. The companion signs the post-quantum domain tag plus the exact classical preimage the response's kid selects, so no new canonicalization was introduced and the v1 and v2 eras are covered identically. The pqJwt is its own compact JWS with the same claims as jwt, and the verifier binds it to the classical token by jti, exp and pass, so a companion cannot be transplanted from another artifact. Before any of it was published we ran the previous release of the verifier against live responses to confirm it returned identical verdicts with and without the new fields.
The binding between the classical key and the post-quantum key is a content-addressed statement at /.well-known/pq-key-binding.json, signed by both keys and anchored on Base in block 50758053. Its block time is the public record of when the companion came into existence, which is what lets a verifier reading an old artifact say, truthfully, that no companion could have existed when it was issued.
The fifth verdict has four states: verified, refuted (present and failing), absent, and unverifiable (present but not checkable, because the key could not be resolved or the runtime has no ML-DSA implementation). The rules: refuted always fails the artifact. Absent or unverifiable fail only under your own pqRequiredFrom cutoff, judged by your clock at verification time and never by any timestamp inside the artifact, because under a classical break every one of those timestamps is forgeable. In mode: "evidence", for reading an artifact after the fact, nothing is refused; the verifier reports the state and, given the anchored binding date, whether a companion existed at issuance.
import { verifyAttestation } from "insumer-verify";
// A live access decision: from this date, insist on the companion.
const r = await verifyAttestation(response, {
jwksUrl: "https://insumermodel.com/.well-known/jwks.json",
pqRequiredFrom: "2027-06-01T00:00:00Z",
});
r.checks.pq.status; // "verified" | "refuted" | "absent" | "unverifiable"
// Reading a receipt years later: never refused, always explained.
const e = await verifyAttestation(response, {
jwksUrl, mode: "evidence", pqActivatedAt: bindingBlockTime,
});
e.checks.pq.existedAtIssuance; // false for anything issued before the binding
ML-DSA is not in Web Crypto yet, so the verifier loads it from an optional dependency and reports unverifiable with a reason when it is missing, never a silent pass and never a silent failure. A PHP verifier can do the same through OpenSSL 3.5 or later on PHP 8.4 or later, and SkyeMeta's plugins do exactly that, probing the host with a real vector at activation and saying plainly when the host cannot.
Test vectors and the multi-issuer envelope
Seventeen published vectors at /.well-known/state-attestation-test-vectors.json, mirrored from the insumer-examples repository, which is the authority if the two ever differ. Eleven cover the classical checks, tampered conditions, tampered signatures and an unresolvable kid. Six cover the companion: both key eras verified, a tampered companion refuted, an unresolvable pqKid under a cutoff, a pre-companion artifact under a cutoff, and the JWT envelope. Each carries a recompute block with the canonical condition, its hash, and the anchor, and states its expected verdicts as separate booleans rather than one. Anyone can run the whole set against any node without anything from us, which is why a run is the runner's to publish.
For stacks that carry attestations from several issuers, the same repository holds a multi-attestation envelope spec and a reference verifier: each slot keeps its own issuer's signature and key identifier, the envelope is transport rather than trust, and the aggregate is recomputed from the slot verdicts plus pinned options. Eight envelope fixtures pin the behaviour, including the two cases that matter most, a slot with its signature stripped and an object stapled beside a JWS that bears no signature at all.
Where it plugs in
Forty-six operations in the OpenAPI spec. An MCP server with twenty-seven tools, a LangChain toolkit with twenty-six, LlamaIndex tools, ElizaOS actions, a ChatGPT action, and the insumer-verify package for the checking side. On the product side, SkyeMeta's SkyeGate and SkyeWoo plugins and SkyeMeta's Vercel SDK consume the JWT format and now report the companion too. None of that is new surface; it is the same primitive packaged for wherever your code already lives.
What to take from this
If you integrate one thing, integrate the verifier and read its five verdicts instead of one boolean. If you integrate two, add a delegation condition in front of anything an agent can spend. And if you keep attestations as evidence, keep the whole envelope: the block, the condition, the hash and both signatures are what make it worth keeping.
Conditions in. Signed attestations out. That has not changed. The list of conditions has.
Douglas
Get every post by email
The Inevitable series and builder notes on condition-based access. Free, one or two posts a week.