<!-- Markdown copy of https://insumermodel.com/developers/verification/ for agents. -->

> Check whether a wallet holds a token or NFT on 37 chains (NFTs on 33) and get a signed yes or no. No balances exposed by default. ERC-20, SPL, XRPL.
>
> Page: https://insumermodel.com/developers/verification/ · API: https://api.insumermodel.com · OpenAPI: https://insumermodel.com/openapi.json · Index for agents: https://insumermodel.com/llms.txt

[Developers](https://insumermodel.com/developers/) > Verification

# On-Chain Attestation

Privacy-preserving boolean attestation across 37 blockchains. Read token balances, NFT ownership, EAS attestations, Farcaster identity, arbitrary boolean view calls, ratio rules that never need re-tuning, and agent standing on Base. Signed attestations back.

Core primitive. Trust profiles, compliance gating, and commerce all extend this signed attestation.

Auth: `X-API-Key`, or `Authorization: Wallet` for EVM agents who bought their key via `/v1/keys/buy`. [See Authentication →](https://insumermodel.com/developers/api-reference/#auth)

**Building this in Claude Code?** Install the wallet auth skill and Claude will write correct, signature-verifying integration code on the first try.

```bash
git clone https://github.com/insumerapi/insumer-skill.git ~/.claude/skills/insumer
```

[View on Smithery](https://smithery.ai/skills/douglasborthwick/insumer-skill) · [GitHub repo](https://github.com/insumerapi/insumer-skill)

## What this does

Check if a wallet meets conditions without exposing balances. The API returns an ECDSA-signed true/false result with condition hashes and block anchoring, so any party can independently verify the attestation without trusting the caller. Trust profiles, compliance gating, and commerce endpoints all build on this same signed attestation model.

## Verify a wallet in one call

Check if a wallet holds at least 1,000 USDC on Ethereum.

```javascript
const res = await fetch("https://api.insumermodel.com/v1/attest", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": "YOUR_API_KEY"
  },
  body: JSON.stringify({
    wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    conditions: [{
      type: "token_balance",
      contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
      chainId: 1,         // Ethereum mainnet
      threshold: "1000"    // decimal string: new keys are v2 and reject a JSON number with 400
    }]
  })
});

const response = await res.json();
console.log(response.data.attestation.pass); // true or false
```

```python
import requests

res = requests.post(
    "https://api.insumermodel.com/v1/attest",
    headers={"x-api-key": "YOUR_API_KEY"},
    json={
        "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
        "conditions": [{
            "type": "token_balance",
            "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",  # USDC on Ethereum
            "chainId": 1,         # Ethereum mainnet
            "threshold": "1000"    # decimal string: new keys are v2 and reject a JSON number with 400
        }]
    }
)

response = res.json()
print(response["data"]["attestation"]["pass"])  # True or False
```

```bash
curl -X POST https://api.insumermodel.com/v1/attest \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    "conditions": [{
      "type": "token_balance",
      "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "chainId": 1,
      "threshold": "1000",
      "label": "USDC >= 1000"
    }]
  }'

# contractAddress: USDC on Ethereum (chainId 1, 6 decimals)
# threshold: a decimal string ("1000"), not a number. New keys are v2 and reject a JSON number with 400; a string also works on legacy v1 keys. The value is in token/display units, not base units or wei: "1000" means 1000 whole USDC and "0.001" means 0.001 ETH. The token's own decimals are read from the chain, so a base-unit threshold is not an error: it is a valid request for a far larger amount, and returns met: false.
```

## Ten ways to verify

Each condition type evaluates wallet state a different way, from a single balance to a relationship like share-of-supply. Combine multiple conditions in a single call.

#### `token_balance`

Check if a wallet holds at least a threshold amount of a token, or of the chain’s own native asset. Works on all **37 chains**: every EVM chain plus Solana, XRP Ledger, Bitcoin, Tron, Stellar and Sui. (Bitcoin is native-only; XRP Ledger trust lines take `currency` and the issuer r-address.)

- `contractAddress` - Token contract, or `"native"` for the chain’s own asset
- `chainId` - Network ID
- `threshold` - Minimum amount in display units, never base units or wei (see above)
- `decimals` - Token decimals (e.g. 6 for USDC). Optional: the token’s own decimals are always read from the chain, and a value you send is a cross-check. If it differs from the token’s own, the API returns 400 and names the token’s own value

**On two chains the native asset is also an ERC-20.** Celo and Arc expose the gas asset at a mirror contract; on Arc that mirror is the USDC predeploy, because USDC *is* Arc’s gas asset. `"native"` and that address are one balance read two ways, so two `token_balance` conditions requiring both would count the same holding twice, and the pair is refused with a `400` naming both condition indexes before any credit is charged. Use one or the other. With `proof: "merkle"`, a `"native"` condition returns an *account* proof (`subject: "account_balance"`) rather than a storage proof.

#### `nft_ownership`

Verify ownership of ERC-721 NFTs. Checks that the wallet holds at least one NFT from the collection.

- `contractAddress` - NFT contract
- `chainId` - Network ID
- `threshold` - Not used: met when the wallet holds at least one

#### `eas_attestation`

Verify on-chain EAS attestations by schema ID or use pre-configured compliance templates. [See compliance docs.](https://insumermodel.com/developers/compliance/)

- `schemaId` - EAS schema UID
- `template` - e.g. `coinbase_verified_account`
- Works with Coinbase, Gitcoin Passport

#### `farcaster_id`

Check if a wallet has a registered Farcaster ID. Uses the IdRegistry contract on Optimism. Returns a boolean indicating registration status.

- No `contractAddress` needed
- No `chainId` needed
- Automatic IdRegistry lookup

#### `ratio_to_amount`

Self-scaling agent-spend rule: met when the wallet holds at least `multiple` times a per-request `amount`. One rule that adjusts to every transaction size, no re-tuning. EVM chains.

- `contractAddress` - Token contract (or `native`)
- `chainId` - EVM network ID
- `multiple` - e.g. 10 for “10× the amount”
- `amount` - Reference amount (token units)

#### `ratio_to_supply`

Share-of-supply rule: met when the wallet holds at least `minFraction` of the token’s on-chain total supply. For project and governance tokens, not stablecoins. EVM chains, ERC-20 only.

- `contractAddress` - ERC-20 token contract
- `chainId` - EVM network ID
- `minFraction` - Fraction in (0,1], e.g. 0.005 = 0.5%

#### `erc8004_agent`

Is this wallet a registered ERC-8004 agent? Met when the wallet owns the agent NFT or is the registry’s signature-verified `agentWallet` binding. Base only. Available on any API key, or with no key at all via x402 pay-per-call.

- `chainId` - `8453` (Base)
- `agentId` - uint256 decimal string, required
- Registration ≠ vetting (see below)

#### `erc7710_delegation`

Is this signed ERC-7710 delegation from principal P to this agent wallet currently valid, and what limits does it declare? Signature, revocation, and time-window checked at the anchored block. Base only. Available on any API key, or with no key at all via x402 pay-per-call.

- `delegationManager` - Recognized manager on Base
- `expectedDelegator` - Asserted principal, required
- `delegation` - The signed delegation object

#### `evm_view_call`

Call any boolean view function on any EVM chain and attest the result. Met when the function returns true for the attested wallet. Use this when a contract already encodes your eligibility rule and you would rather read it than restate it. Single-address-argument view functions only.

- `contractAddress` - Contract to call
- `chainId` - EVM network ID
- `selector` - Signature, e.g. `hasAccess(address)`

#### `account_code`

What code sits at the wallet address itself at the anchored block? Met when the address is in the state named by `expect`: a plain key account (no code), an 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 three are exclusive on a chain. Any EVM chain. The answer is the boolean only; the code and the delegation target are never returned.

- `chainId` - EVM network ID
- `expect` - `"none"`, `"eip7702"` or `"contract"`, required
- `delegate` - With `"eip7702"` only: an address the designator must point at

## POST /v1/attest

Boolean attestation. Returns an ECDSA-signed pass/fail for one or more conditions.

### Request Parameters

| Parameter | Type | Required | Description |
|---|---|---|---|
| `wallet` | string | Yes* | EVM wallet address (0x...) |
| `solanaWallet` | string | Yes* | Solana wallet address (base58) |
| `xrplWallet` | string | Yes* | XRPL wallet address (r-address) |
| `bitcoinWallet` | string | Yes* | Bitcoin address (P2PKH, P2SH, bech32, or Taproot) |
| `tronWallet` | string | Yes* | Tron address (T-prefix, base58, 34 chars) |
| `stellarWallet` | string | Yes* | Stellar account address (G-prefix, 56 chars) |
| `suiWallet` | string | Yes* | Sui address (0x + 64 hex chars) |
| `conditions` | array | Yes | Array of condition objects |
| `proof` | string | No | Set to `"merkle"` for storage proofs: balance slots on `token_balance`, the account record with `codeHash` as the proven value on `account_code`, the revocation slot on `erc7710_delegation`. 2 credits |
| `format` | string | No | Set to `"jwt"` for a Wallet Auth JWT (ES256-signed). No additional cost |
| `declaredLimits` | string | No | `"include"` (default) or `"omit"`. Set to `"omit"` to leave decoded caveat limits out of `erc7710_delegation` results (see Agent conditions below). No additional cost |

* Provide `wallet` for EVM chains, `solanaWallet` for Solana, `xrplWallet` for XRP Ledger, `bitcoinWallet` for Bitcoin, `tronWallet` for Tron, `stellarWallet` for Stellar, or `suiWallet` for Sui. Use one or more as needed.

### Condition Object Fields

| Field | Type | Description |
|---|---|---|
| `type` | string | One of: `token_balance`, `nft_ownership`, `eas_attestation`, `farcaster_id`, `evm_view_call`, `ratio_to_amount`, `ratio_to_supply`, `erc8004_agent`, `erc7710_delegation`, `account_code` |
| `contractAddress` | string | Token or NFT contract address. Solana: `"native"` for SOL, or the mint address (base58, 32 to 44 characters); anything else is a 400. XRPL: `"native"` for XRP, or issuer r-address for trust line tokens and NFTs. `"native"` is not an NFT: on `nft_ownership` it is a 400. Sui: a coin type (`address::module::Name`, optionally with type parameters); the short form (`0x2::sui::SUI`) and the 64-hex long form of an address name the same coin, and anything that is not a coin type is a 400 |
| `chainId` | number \| string | Network chain ID (e.g. `1` for Ethereum, `"solana"`, `"xrpl"`) |
| `threshold` | string (v2 keys) / number (v1 keys) | Minimum amount in display units, as a decimal string (e.g. `"1000"`). `token_balance` only. A decimal string is at most 400 characters; the same holds for `multiple`, `amount` and `minFraction` |
| `multiple` | string (v2 keys) / number (v1 keys) | `ratio_to_amount` only. Met iff balance ≥ multiple × amount (e.g. `10`). Must be > 0 |
| `amount` | string (v2 keys) / number (v1 keys) | `ratio_to_amount` only. Reference amount in token/display units (e.g. `100` for 100 USDC, not base units). Must be > 0. An amount with more decimal places than the token has is a 400 |
| `minFraction` | string (v2 keys) / number (v1 keys) | `ratio_to_supply` only. Required share of total supply, a fraction in (0, 1] (e.g. `0.005` for 0.5%). Must be > 0 |
| `decimals` | number | Token decimals (e.g. 18 for ETH, 6 for USDC). Optional. The token’s own decimals are always read from the chain (`decimals()` on EVM and Tron at the anchored block, the coin metadata on Sui; native coins are fixed by definition), so a scale is never taken from the request and never assumed. When sent it is a cross-check: an integer from 0 to 100 (a JSON number, or its digits as a string), anything else is a 400; equal to the token’s own value, nothing changes; different, the API returns 400 and the message names the token’s own value. If the token’s decimals cannot be read, that is the 503 `rpc_failure`, never a 400. A threshold is in display units, so the verdict depends on the scale, and the relying party must be able to trust that the scale is the token’s own |
| `currency` | string | XRPL currency code (e.g. `"RLUSD"`): a 3-character standard code, a token name of 1 to 20 printable ASCII characters, or a 40-character hex code. Currency codes are case-sensitive: send the code exactly as issued. `XRP` is the native coin, not a trust line currency: to check XRP, use `contractAddress` `"native"`. Required for XRPL trust line tokens |
| `taxon` | integer | XRPL NFT taxon filter: an integer from 0 to 4294967295, as a JSON number or its digits as a string. Anything else is a 400. Optional. If omitted, matches any NFT from the issuer |
| `assetCode` | string | Stellar trustline asset code (e.g. `"USDC"`): 1 to 12 characters from a-z, A-Z, 0-9. Anything else is a 400. Required for Stellar trustline tokens, with the issuer G-address as `contractAddress`. Not used for native XLM |
| `template` | string | Compliance template name (EAS only) |
| `schemaId` | string | EAS schema UID (EAS only) |
| `attester` | string | Expected attester address (EAS only, optional) |
| `indexer` | string | EAS indexer contract address (EAS only). Required for a raw `schemaId` condition: without it the request is a 400. Not needed with `template` |
| `agentId` | string | `erc8004_agent` only. uint256 as a decimal string (e.g. `"123"`). Required: the deployed Identity Registry has no wallet-to-agentId reverse lookup |
| `delegationManager` | string | `erc7710_delegation` only. Must be one of the three recognized MetaMask Delegation Framework managers on Base (versions 1.0.0 / 1.1.0 / 1.3.0) |
| `expectedDelegator` | string | `erc7710_delegation` only. Required: the principal the caller asserts authorized this agent |
| `delegation` | object | `erc7710_delegation` only. The signed delegation: `delegator`, `delegate`, `authority` (root only), `caveats` (max 16), `salt`, `signature` |
| `expect` | string | `account_code` only. Required: the code state the wallet address itself must be in at the anchored block. `"none"` (no code: a plain key account), `"eip7702"` (the EIP-7702 delegation designator: a key that has delegated execution to a contract) or `"contract"` (any other code: a smart-contract wallet, a protocol, a token). The three are exclusive on a chain. EVM chains only; a non-EVM `chainId` is a 400. The result is the boolean `met`; the code and the delegation target are never returned |
| `delegate` | string | `account_code` with `expect: "eip7702"` only (a 400 with any other `expect`). An EVM address: the condition is met iff the wallet’s designator points at it. When supplied it is echoed (lowercase) inside the signed `evaluatedCondition`, so the hash binds the question asked |
| `label` | string | Optional human-readable label |

### Response

200 OK:

```json
{
  "ok": true,
  "data": {
    "attestation": {
      "id": "ATST-E365DA8B1C2F4790",
      "pass": true,
      "results": [
        {
          "condition": 0,
          "label": "USDC >= 1000",
          "type": "token_balance",
          "chainId": 1,
          "met": true,
          "evaluatedCondition": {
            "type": "token_balance",
            "chainId": 1,
            "contractAddress": "0xA0b8...eB48",
            "operator": "gte",
            "threshold": "1000"
          },
          "conditionHash": "0x448ddd3e...",
          "blockNumber": "0x1772dde",
          "blockTimestamp": "2026-03-05T00:41:11.000Z"
        }
      ],
      "passCount": 1,
      "failCount": 0,
      "attestedAt": "2026-03-05T00:41:14.934Z",
      "expiresAt": "2026-03-05T01:11:14.934Z"
    },
    "sig": "rHObYHqV...",
    "kid": "insumer-attest-v2",
    "pqSig": "...",
    "pqKid": "insumer-attest-pq1"
  },
  "meta": {
    "creditsRemaining": 99,
    "creditsCharged": 1,
    "version": "1.0",
    "timestamp": "2026-03-05T00:41:14.934Z"
  }
}
```

**pass** is `true` only when ALL conditions are met.

**sig** is an ECDSA P-256 signature over the attestation payload.

**kid** identifies the signing key. Fetch the public key from `/.well-known/jwks.json`.

Standard attestation: 1 credit ($0.04). With Merkle proofs: 2 credits ($0.08).

#### XRPL Response Fields

XRPL Trust Line Token Result:

```json
{
  "condition": 0,
  "label": "RLUSD >= 50",
  "type": "token_balance",
  "chainId": "xrpl",
  "met": true,
  "evaluatedCondition": {
    "type": "token_balance",
    "chainId": "xrpl",
    "contractAddress": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De",
    "currency": "RLUSD",
    "threshold": "50"
  },
  "conditionHash": "0x7f2a...",
  "ledgerIndex": 95482163,
  "ledgerHash": "BB9023D447285923...",
  "trustLineState": { "frozen": false }
}
```

**ledgerIndex** + **ledgerHash** replace `blockNumber`/`blockTimestamp` for XRPL conditions. They identify the validated ledger at verification time.

**trustLineState** is present only for non-native XRPL token conditions. Shows whether the trust line is frozen.

**Frozen trust lines** return `met: false` regardless of balance: frozen tokens are not spendable.

## Agent conditions: standing to act

InsumerAPI attests to on-chain state and takes no side in the transaction. At settlement time three questions exist: can this wallet pay (payment rails), is the decision sound (reasoning verifiers), and does this party have standing to act. These two condition types answer the third, straight from chain state. Both are generally available, so every API key can send them with no flag to request. Base (chainId `8453`) only. 1 credit each. `erc8004_agent` does not support Merkle proofs; `erc7710_delegation` does: a revocation proof, covered below.

They also work with no API key at all, via x402 pay-per-call: send `POST /v1/attest` with no credential headers, take the `402` quote, pay the quoted amount in USDC on Base, Polygon, Arbitrum, Arc, or Solana, and retry with the `PAYMENT-SIGNATURE` header. A standard call prices at the `$0.05` rate. An agent can ask whether a counterparty has standing to act without ever opening an account.

### `erc8004_agent`

Is this wallet a registered ERC-8004 agent? Checked against the ERC-8004 Identity Registry at `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432` on Base.

Request condition:

```json
{
  "type": "erc8004_agent",
  "chainId": 8453,
  "agentId": "123",
  "label": "Registered agent"
}
```

**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`). tokenURI JSON is never trusted.

**agentId** is required (uint256 decimal string): the deployed registry has no wallet-to-agentId reverse lookup.

**Result extras** (inside the signed results array): `agentExists` (boolean), `matchedVia` (`"owner"` | `"agent_wallet"` | `"none"`). The `evaluatedCondition` carries `operator: "owner_or_bound_wallet"`.

**Honest semantics:** 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`

Is this signed delegation from principal P to this agent wallet currently valid, and what limits does it declare? Verified against a recognized MetaMask Delegation Framework manager on Base (versions 1.0.0 / 1.1.0 / 1.3.0). Root authority only: delegation chains are unsupported in v1. Max 16 caveats, max 3 delegation conditions per request.

Request condition:

```json
{
  "type": "erc7710_delegation",
  "chainId": 8453,
  "delegationManager": "0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3",
  "expectedDelegator": "0x<principal address>",
  "delegation": {
    "delegator": "0x<principal>",
    "delegate": "0x<agent wallet>",
    "authority": "0xffff...ffff",
    "caveats": [
      { "enforcer": "0x<enforcer>", "terms": "0x<hex>" }
    ],
    "salt": "42",
    "signature": "0x<hex>"
  },
  "label": "Authorized by principal"
}
```

**met** is `true` iff ALL of: the attested wallet is the delegate; the declared delegator is `expectedDelegator`; the EIP-712 signature verifies (EOA recovery, or ERC-1271 for contract principals); the delegation is not revoked on-chain as of the anchored block; every caveat uses a recognized enforcer (an unknown enforcer fails the condition, no override); and any time-window caveat is currently satisfied.

**expectedDelegator** is required: without it a self-delegation would read as authority, so there is no structural-only mode.

**Recognized enforcers** (5 kinds): timestamp (time window, evaluated now), erc20_transfer_amount, native_transfer_amount, allowed_targets, limited_calls. The last four are *reported* as declared limits: on-chain redemption enforces them, and the attestation states what the principal signed rather than simulating enforcement.

**signatureType** semantics: `"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.

**Result extras:** `declaredLimits` (decoded caveat limits, decimal-string amounts, each entry carrying the raw `terms` hex it was decoded from, and not part of the `met` verdict; see below), `unrecognizedEnforcers` (when failing on unknown caveats), `failReason` (`delegate_mismatch`, `principal_mismatch`, `invalid_signature`, `delegator_not_deployed`, `revoked`, `unknown_caveat_enforcer`, `outside_time_window`). `delegator_not_deployed` means the principal address held no contract code at the anchored block: the state a smart-contract wallet is in until its first transaction. It is kept separate from `invalid_signature` because no signature was evaluated, so your code can retry once the principal deploys instead of treating it as a bad signature.

**5-minute expiry:** attestations containing a delegation condition expire in 5 minutes, not the standard 30: revocation is one transaction away, so the verdict window stays tight. The verdict states “not revoked as of block N” (`blockNumber` is in the signed result).

**Revocation proofs:** add `proof: "merkle"` and the result carries an EIP-1186 storage proof of the revocation slot (`subject: "delegation_revocation"`, 2 credits). “Not revoked as of block N” stops being something you take our word for. See below.

### The verdict and the declared limits are two different things

`met` is the verdict, and it is exactly the boolean above: the attested wallet is the delegate, the declared delegator matches `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 currently satisfied.

`declaredLimits` is *not* part of that boolean. It is a signed *decode* of the caveat terms the caller itself submitted, riding alongside the verdict inside the signed results: a sibling of `evaluatedCondition`, inside the signed payload but outside `conditionHash`. InsumerAPI returns signed booleans, never raw wallet data. On a delegation, the boolean is `met`, and the declared limits travel next to it, signed. They ride on passing and failing verdicts alike, once the signature and revocation checks pass: a delegation failing on `unknown_caveat_enforcer` or `outside_time_window` still carries the decode of its recognized caveats, while earlier failures (bad signature, revoked, mismatched parties) carry none. Worth knowing which is which before you build on either.

#### One decoded entry

declaredLimits[0]:

```json
{
  "kind": "erc20_transfer_amount",
  "enforcer": "0xf100b0819427117ecf76ed94b358b1a5b5c6d2fc",
  "terms": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000000000000000003b9aca00",
  "token": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  "maxAmount": "1000000000"
}
```

All numeric values are decimal strings in base units: `"1000000000"` here is 1,000 USDC at 6 decimals.

#### Recognized kinds

| kind | Fields |
|---|---|
| `timestamp` | `timestampAfter`, `timestampBefore` (the one kind the verdict also evaluates) |
| `erc20_transfer_amount` | `token`, `maxAmount` |
| `native_transfer_amount` | `maxAmount` |
| `allowed_targets` | `targets` (array of addresses) |
| `limited_calls` | `maxCalls` |

#### Check the decode yourself

Every entry carries `terms`: the exact hex bytes it was decoded from. You supplied those bytes in your own request, so verification needs no external lookup and no second API call.

1. Re-run the enforcer contract’s public `getTermsInfo` byte layout against `terms`, and compare the result to the decoded values in the entry. In the example above, the first 20 bytes are the token and the next 32 are the amount.
2. Recompute `delegationHash` (the EIP-712 struct hash, carried in the signed `evaluatedCondition`, that commits to every caveat’s enforcer and terms) from the delegation object you already hold, and confirm it matches.

Together those two steps make the decode a checkable claim rather than a trust-us assertion: the hash pins which caveats were evaluated, and the byte layout pins what each one says.

#### Leaving the limits out: `declaredLimits: "omit"`

A delegation attestation in JWT form is portable, which means that by default the principal’s declared limits travel to anyone the token is forwarded to. Usually that is exactly right: a relying party needs the limits to act on them. Sometimes it is not.

Send the top-level `declaredLimits` modifier (a sibling of `proof` and `format`) to control it. Absent or `"include"` is the default: the limits are returned in the signed results, and therefore travel inside the JWT when `format: "jwt"` is used. Set it to `"omit"` and the decoded limits are left out; the result carries `declaredLimitsOmitted: true` instead.

Request:

```json
{
  "wallet": "0x<agent wallet>",
  "format": "jwt",
  "declaredLimits": "omit",
  "conditions": [ { "type": "erc7710_delegation", ... } ]
}
```

You lose nothing by omitting: you submitted the caveats, so you already hold them. And the guarantee that makes this safe to forward: `met`, `delegationHash`, and `conditionHash` are byte-identical whether or not the limits are omitted. A forwarded token still commits to exactly which delegation was checked, and a holder can verify a limit shown to them out-of-band without ever learning one from the token.

### The last part of the verdict is provable too

A delegation verdict has three parts. The EIP-712 signature check you can reproduce yourself: you hold the delegation object. The caveat decode you can reproduce yourself: you submitted the raw `terms` bytes, and the steps above show how. Without a proof, “not revoked as of block N” is ours to assert and yours to accept.

Send `proof: "merkle"` alongside a delegation condition and that gap closes. The result carries an EIP-1186 storage proof of `disabledDelegations[delegationHash]` in the DelegationManager’s storage, against the anchored block’s state root. Verify it against a block header and the whole verdict is checkable rather than partly trusted.

results[0].proof:

```json
{
  "available": true,
  "type": "merkle",
  "subject": "delegation_revocation",
  "blockNumber": "0x12a05f200",
  "contractAddress": "0xdb9b1e94b5b69df7e401ddbede43491141047db3",
  "mappingSlot": 4,
  "storageKey": "0x<keccak256(abi.encode(delegationHash, mappingSlot))>",
  "accountProof": [ "0x...", ... ],
  "storageProof": [ { "key": "0x...", "value": "0x0", "proof": [ "0x...", ... ] } ],
  "storageHash": "0x..."
}
```

`subject` is what tells a delegation proof apart from a balance proof: balance proofs carry no `subject` field at all.

#### How to check it

1. Recompute `storageKey = keccak256(abi.encode(delegationHash, mappingSlot))` using the `delegationHash` from the signed `evaluatedCondition`, and confirm it equals the returned `storageKey`. Do not skip this step: it is what binds the proof to *this* delegation. A proof of some other slot would verify against the header perfectly well.
2. Verify `accountProof` and `storageProof` against the state root of the block header for `blockNumber`.
3. Read the proven value: `1` means revoked, `0` means not revoked. Absence is proven here as firmly as presence: a proven `0` is positive evidence that no revocation exists, not a report that none was found.

#### Which managers

Proofs are returned only for DelegationManager deployments whose storage layout has been verified on-chain. Today that is the v1.3.0 manager on Base, `0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3`. The other recognized manager versions return `proof.available: false` with a reason saying the layout is unverified; the boolean verdict is computed and signed exactly as normal.

That restraint is the point. An inferred slot number would produce a proof of the wrong storage location that still verifies cleanly against the block header, a confident answer to a question nobody asked. Worse than no proof at all. So a slot gets proven only once it has been confirmed against the deployed contract.

Proof mode on a delegation condition costs 2 credits instead of 1, the same premium as any other proof-mode request. Whenever no proof is delivered the premium is refunded and the call costs 1, whatever the reason.

## Four layers of independent verification

Every attestation can be checked offline against our published keys, and keys are never removed, so attestations you keep as records stay checkable. One API call, four verification layers.

### ECDSA Signature Verification

Every attestation is signed with ECDSA P-256 and also carries a post-quantum signature (ML-DSA-65) as `pqSig`/`pqKid`, reported by verifiers as a separate verdict. The algorithm is [swappable via JWKS](https://insumermodel.com/blog/quantum-breaks-static-credentials-wallet-auth.html) without breaking verifiers. Verify the signature using the `insumer-verify` library or raw Web Crypto. The same verifier is on PyPI for Python 3.9+ (`pip install insumer-verify`, add `[pq]` for the post-quantum check); both packages pass the same 27 published test vectors.

```javascript
// npm install insumer-verify @noble/post-quantum
// The PQ peer is optional. Without it the post-quantum check reports
// status "unverifiable" rather than verifying it.
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://insumermodel.com/.well-known/jwks.json"
});

console.log(result.valid); // true
```

Web Crypto API:

```javascript
// V1-ONLY WALKTHROUGH. This covers pre-cutover insumer-attest-v1
// responses only. Keys minted today sign with kid insumer-attest-v2
// over a domain-separated, canonical preimage: use insumer-verify for
// those, or follow the verification algorithm in the OpenAPI spec.
const data = (await res.json()).data;
if (data.kid !== "insumer-attest-v1") throw new Error("v2 response: use insumer-verify");

// Fetch the public key from JWKS, selected by the kid on the response
const jwks = await fetch("https://insumermodel.com/.well-known/jwks.json")
  .then(r => r.json());
const key = jwks.keys.find(k => k.kid === data.kid);

// Import the public key
const publicKey = await crypto.subtle.importKey(
  "jwk", key,
  { name: "ECDSA", namedCurve: "P-256" },
  false, ["verify"]
);

// Reconstruct the signed bytes. v1 signs exactly these four fields,
// keys in their original order. Do NOT sort them.
const a = data.attestation;
const payload = new TextEncoder().encode(JSON.stringify(
  { id: a.id, pass: a.pass, results: a.results, attestedAt: a.attestedAt }
));
const sigBytes = Uint8Array.from(
  atob(data.sig), c => c.charCodeAt(0)
);

const valid = await crypto.subtle.verify(
  { name: "ECDSA", hash: "SHA-256" },
  publicKey, sigBytes, payload
);
console.log(valid); // true
```

Cryptographic proof

### Condition Hash Integrity

Each result includes a `conditionHash` computed from `evaluatedCondition`. Recompute it yourself to confirm the attestation matches the condition you requested.

Recompute conditionHash:

```javascript
const condition = result.evaluatedCondition;
const sortedKeys = Object.keys(condition).sort();
const canonical = JSON.stringify(condition, sortedKeys);
const hashBuffer = await crypto.subtle.digest(
  "SHA-256",
  new TextEncoder().encode(canonical)
);
const hashHex = "0x" + [...new Uint8Array(hashBuffer)]
  .map(b => b.toString(16).padStart(2, "0"))
  .join("");

console.log(hashHex === result.conditionHash); // true
```

Audit trail

### Merkle Storage Proofs

Add `proof: "merkle"` to get an EIP-1186 storage proof anchored to a block state root. Verify the balance against that block’s state root, with no call to us. Available on 27 of 31 EVM chains for `token_balance`, `ratio_to_amount`, `account_code` and `erc7710_delegation`. A condition with `contractAddress: "native"` carries an **account** proof instead (`subject: "account_balance"`): a native balance is a field of the account rather than an entry in a mapping, so the proof is the account’s own branch and its balance field is the value evaluated. There is no storage slot to discover, and it proves a zero balance as readily as a non-zero one. Also on `erc7710_delegation` conditions, where the proof covers the revocation slot instead of a balance slot, and on `account_code` conditions, which carry the same account proof as a native balance with `subject: "account_code"` and `codeHash` as the proven value: keccak256 of empty code means no code, keccak256 of `0xef0100` followed by the 20-byte target means an EIP-7702 delegation to that target (checkable only with the delegate the verifier supplies), anything else means contract code; an account absent from the state trie may report all zeros, which also means no code.

```bash
curl -X POST https://api.insumermodel.com/v1/attest \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    "proof": "merkle",
    "conditions": [{
      "type": "token_balance",
      "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "chainId": 1,
      "threshold": "1000",
      "label": "USDC >= 1000"
    }]
  }'

# Response includes a proof object with storageProof, storageHash,
# and blockNumber alongside the standard attestation fields
```

Merkle proofs cost 2 credits ($0.08) per attestation; whenever no proof is delivered the premium is refunded and the call costs 1, whatever the reason: a read that did not complete, a chain or token that cannot carry a proof, or a condition type that has none. The response includes a `proof` object containing the raw `storageProof` array, `storageHash`, and `blockNumber`, allowing full trustless verification against the chain state. An **account** proof (`subject: "account_balance"`, returned for `contractAddress: "native"`) carries `accountProof`, `balance`, `nonce` and `codeHash` instead, and no `storageProof` or `mappingSlot`; on an `account_code` condition the same record carries `subject: "account_code"` and its `codeHash` is the value evaluated. On a delegation condition the object also carries `subject: "delegation_revocation"` and the `contractAddress` of the DelegationManager (see Agent conditions for the verification steps).

Trustless verification

### Wallet Auth JWT

Add `format: "jwt"` to receive an ES256-signed JWT alongside the standard response, verifiable by any standard JWT library using the JWKS at `/.well-known/jwks.json`. A sibling `pqJwt` (ML-DSA-65) comes beside it; checking the post-quantum signature takes an ML-DSA-65 verifier such as `insumer-verify`, which binds it to the `jwt` by the whole claim set. See the full Wallet Auth section below.

Standard JWT verification

### Keeping attestations as records

Keys are never removed from our JWKS, so an attestation you keep stays checkable. To check it without relying on us at all, save a copy of the JWKS with your stored attestations and verify against that copy. The saved set covers both the ECDSA signature and the post-quantum signature, and nothing is fetched. `insumer-verify` 1.9.0 and later also ship a snapshot in `keys/jwks.json`, together with `keys/pq-key-binding.json`, whose digest is anchored on Base. In Python, pass the saved set as `jwks=` to `verify_attestation`; the Python package honours it the same way and fetches nothing.

Use only a copy you saved from our own endpoints: a verifier believes whatever keys you give it. An old attestation still fails the expiry check. Keeping the key keeps the signature checkable; it does not make the attestation valid for access again.

```javascript
// npm install insumer-verify@^1.9.0 @noble/post-quantum
import { readFileSync } from "node:fs";
import { verifyAttestation } from "insumer-verify";

// Saved when you stored the record, from https://insumermodel.com/.well-known/jwks.json
const savedJwks = JSON.parse(readFileSync("./records/insumer-jwks.json", "utf8"));
const storedResponse = JSON.parse(readFileSync("./records/stored-response.json", "utf8"));

// Nothing is fetched. Evidence mode reports every verdict for an old record.
const result = await verifyAttestation(storedResponse, { jwks: savedJwks, mode: "evidence" });
console.log(result.checks.signature.passed, result.checks.pq.status, result.checks.expiry.passed);
// true verified false
```

Offline, no call to us

### What a signed result shows, and what it does not

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:

- **How far you have to trust the issuer.** With a Merkle proof (`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.
- **Whose wallet was read.** The raw format signs no wallet for most condition types. Request `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.
- **Who controls the wallet.** The wallet comes from the request body. The API does not check that the caller, or anyone, controls it. If your gate needs that, collect a wallet signature yourself.
- **Who the person is.** The API sees a wallet address and public chain state, and makes no identity statement of its own. An `eas_attestation` result says only that a third party's attestation exists for the wallet.
- **Current state.** The result describes state at the anchor it carries and is valid until `expiresAt`. A wallet can change inside that window. A new call reflects the change.
- **The same anchor on every chain.** On EVM chains and XRPL the result names the exact block or ledger it describes (`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.
- **That a `label` is true.** A label is signed, but the caller writes it. Compare `evaluatedCondition` or `conditionHash` with the conditions your route requires.
- **Spending limits as a verdict.** For `erc7710_delegation`, `declaredLimits` is a signed decode of what the caller submitted. It is not part of `met`.
- **Fields outside the signature.** `expiresAt`, `passCount` and `failCount` are not signed in the raw format. Derive counts from `results`; `expiresAt` is bound to `attestedAt` by the verifier.
- **A record held by InsumerAPI.** InsumerAPI keeps a record that a check took place, its result and when, tied to the API key or payer, but not the wallet, the conditions or any balance. It cannot reproduce a result later. Keep the response, with a copy of the JWKS, if you will need it.

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

## Gate any API on wallet state

Every existing auth system proves who you are. Wallet Auth proves what you hold. It is the primitive of condition-based access: access is gated on wallet state (what a wallet holds, what it has staked, what attestations it carries) rather than identity.

Call `POST /v1/attest` with `format: "jwt"`. InsumerAPI reads the blockchain, evaluates the conditions, and returns an ES256-signed JWT alongside the standard attestation response. Point any standard API gateway at the JWKS endpoint. No blockchain infrastructure needed on the verifying side. No balance exposure. No additional cost: the same 1 credit as a standard attestation.

**Gateway compatible.** Wallet Auth JWTs are standard ES256 JWTs backed by a JWKS endpoint. Compatible with any system that accepts JWKS-backed JWTs:

Kong · Nginx (ngx_http_auth_jwt) · Cloudflare Access · AWS API Gateway · Azure API Management · Traefik · Envoy · any OAuth 2.0 middleware

### 1. Request a Wallet Auth JWT

Add `"format": "jwt"` to any attestation request. The response includes a `jwt` field alongside the standard `sig` and `kid`.

```bash
curl -X POST https://api.insumermodel.com/v1/attest \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    "format": "jwt",
    "conditions": [{
      "type": "token_balance",
      "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "chainId": 1,
      "threshold": "1000",
      "label": "USDC >= 1000"
    }]
  }'

# Response includes data.jwt alongside data.sig and data.kid, plus
# data.pqJwt, the same claims under the post-quantum key
# (alg ML-DSA-65, kid insumer-attest-pq1).
# The jwt field is the Wallet Auth token, an ES256-signed JWT string.
#
# To check pqJwt: verify the JWS, then confirm its payload and the
# jwt payload carry an IDENTICAL claim set - same member names, a
# deeply equal value for every member. Compare parsed JSON, never
# bytes. Any changed, missing or extra member on either side means
# refuted. Checking only jti/exp/pass leaves results and
# conditionHash unbound. Spec Section 12, Check 6.
```

### 2. Verify with insumer-verify

Pass either the full response object or the JWT string directly: `insumer-verify` (1.3.0 and later) auto-detects the format.

```javascript
// npm install insumer-verify @noble/post-quantum
// The PQ peer is optional. Without it the post-quantum check reports
// status "unverifiable" rather than verifying it.
import { verifyAttestation } from "insumer-verify";

// Option A: pass the full API response envelope
const response = await res.json();
const result = await verifyAttestation(response, {
  jwksUrl: "https://insumermodel.com/.well-known/jwks.json"
});

// Option B: pass just the JWT string
// Pass pqJwt too, or the post-quantum signature is reported "absent" and a
// pqRequiredFrom policy will refuse a genuine attestation.
const jwtResult = await verifyAttestation(response.data.jwt, {
  jwksUrl: "https://insumermodel.com/.well-known/jwks.json",
  pqJwt: response.data.pqJwt
});

console.log(result.valid);  // true
console.log(result.checks); // per-check breakdown (signature, conditionHashes, freshness, expiry, pq; plus jwt when the response carries tokens)
```

### 3. Decode JWT claims

The JWT is a standard three-part token. Decode the payload to see the claims structure. Any JWT library in any language can read these claims.

JWT Payload (decoded):

```json
{
  "iss": "https://api.insumermodel.com",       // issuer
  "sub": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",                       // wallet a condition evaluated
  "jti": "ATST-A7C3E1B2D4F56789",                           // attestation ID
  "iat": 1741089600,                              // issued at (unix)
  "exp": 1741091400,                              // expires in 30 min
  "pass": true,                                   // all conditions met
  "conditionHash": ["0x3a7f..."],              // array of all hashes
  "blockNumber": "0x12a3b4f",                    // from first result
  "blockTimestamp": "2026-03-04T12:00:00.000Z", // from first result
  "results": [                                    // per-condition results
    {
      "condition": 0,
      "label": "USDC >= 1000",
      "type": "token_balance",
      "chainId": 1,
      "met": true,
      "evaluatedCondition": { "type": "token_balance", ... },
      "conditionHash": "0x3a7f...",
      "blockNumber": "0x12a3b4f",
      "blockTimestamp": "2026-03-04T12:00:00Z"
    }
  ]
}
```

### 4. Portable attestation

A Wallet Auth JWT can be forwarded from Service A to Service B and verified without re-querying the chain. The receiving service fetches the JWKS public key once, checks the ES256 signature, reads the claims, and makes an access decision, all without any blockchain infrastructure, any Insumer SDK, or any network call back to InsumerAPI. The JWT is self-contained proof of wallet state at a specific block height, valid for 30 minutes.

Service A

Calls `POST /v1/attest`

with format: "jwt"

JWT

Signed attestation

ES256 · 30 min TTL

Service B

Verifies via JWKS

No chain query needed

**Zero dependencies on the verifying side.** Service B needs no blockchain node, no Insumer SDK, and no API key. Standard JWT verification with any language or gateway.

**Use case:** An AI agent obtains a Wallet Auth JWT from InsumerAPI and presents it as a bearer token when calling a downstream API. The downstream API verifies the JWT against the JWKS endpoint and grants access based on the `pass` claim, with no blockchain interaction required.

JWKS endpoint: `https://insumermodel.com/.well-known/jwks.json`, also available at `GET /v1/jwks`. Key IDs: `insumer-attest-v1`, `insumer-attest-v2` and `insumer-trust-v2` over one key, followed by two RFC 9964 `AKP` entries, `insumer-attest-pq1` and `insumer-trust-pq1`, for the ML-DSA-65 post-quantum key. Resolve by the `kid` (and `pqKid`) on the response, never by position.

## Handling rpc_failure errors

If we can’t verify, we don’t sign. When a data source is unavailable after retries, the API returns `503` with error code `rpc_failure`. No attestation signed, no JWT issued, no credits charged.

503: rpc_failure:

```json
{
  "ok": false,
  "error": {
    "code": "rpc_failure",
    "message": "Unable to verify all conditions — data source unavailable after retries",
    "failedConditions": [
      { "source": "balance_read", "chainId": 1, "message": "data source unreachable" }
    ]
  },
  "meta": { "version": "1.0", "timestamp": "..." }
}
```

**This is NOT a verification failure.** Do not treat it as `pass: false`. It means the data source was temporarily unavailable and the API refused to sign an unverified result.

**Retryable.** Retry after 2–5 seconds. The `failedConditions` array names the operation and the chain each failed read was scoped to. Most refusals clear on retry; a few repeat for the same wallet. If the same request is refused again with the same `failedConditions`, stop retrying and treat the result as unavailable, never as `pass: false`.

**Applies to:** `POST /v1/attest`, `POST /v1/trust`, `POST /v1/verify`, `POST /v1/acp/discount`, `POST /v1/ucp/discount`, `GET /v1/discount/check`. `POST /v1/trust/batch` returns 200 instead: a wallet whose reads failed appears as an `error` entry in the results array, beside the wallets that were signed.

## Related articles

#### Merkle Proofs: Trustless On-Chain Verification

How EIP-1186 storage proofs let you verify token balances against the Ethereum state trie without trusting any intermediary.

[Merkle Proofs: Trustless On-Chain Verification](https://insumermodel.com/blog/merkle-proofs-trustless-on-chain-verification.html)

#### Attestation API: Verify Anything On-Chain

A deep dive into boolean attestation, condition types, ECDSA signing, and how the verification endpoint works under the hood.

[Attestation API: Verify Anything On-Chain](https://insumermodel.com/blog/attestation-api-verify-anything-on-chain.html)

#### Check NFT Ownership with Wallet Auth (Python, curl, JS)

Verify if a wallet owns an NFT from any collection across 33 chains. One API call, boolean result, no balance exposed.

[Check NFT Ownership with Wallet Auth (Python, curl, JS)](https://insumermodel.com/blog/check-nft-ownership-api.html)

#### DAO Voting Eligibility with Wallet Auth

Verify governance token holdings server-side for voting eligibility. Working code examples in Python, curl, and JavaScript.

[DAO Voting Eligibility with Wallet Auth](https://insumermodel.com/blog/dao-voting-eligibility-verification.html)

#### Token-Gated API Access with Wallet Auth: JWT Tutorial

Gate your API endpoints on wallet token holdings using JWT bearer tokens. Complete end-to-end tutorial.

[Token-Gated API Access with Wallet Auth: JWT Tutorial](https://insumermodel.com/blog/token-gated-api-access-jwt-tutorial.html)

See how this endpoint fits the full API → [API Topology](https://insumermodel.com/workbench/)

[← Back to Developers Hub](https://insumermodel.com/developers/) [Next: Wallet Trust Profiles →](https://insumermodel.com/developers/trust/)
