Skill instructions
insumer-attest13.2 KB
View saved version →
---
name: insumer-attest
description: >
Wallet auth via InsumerAPI: condition-based access with ES256-signed,
JWKS-verifiable boolean responses (token balances across 37 chains, plus NFT
ownership, EAS attestations, Farcaster IDs and other condition types). Use
when the user wants InsumerAPI to give a "verified yes or no" on whether a
wallet satisfies an on-chain condition, to gate a feature by what a wallet
holds with InsumerAPI, or to compose an InsumerAPI wallet_state signal in a
multi-issuer trust envelope. Read, evaluate, sign, in one call.
metadata:
version: "0.3.2"
author: InsumerAPI
---
# InsumerAPI Wallet Condition Attestation
**Boolean, not balance.** A signed `true`/`false` answering "does this wallet satisfy this condition right now?", never the underlying balance. The response is signed twice (ES256 and a post-quantum ML-DSA-65 signature) and verifiable offline against the public JWKS at `https://insumermodel.com/.well-known/jwks.json`. The signing key never leaves the issuer; anyone holding the JWKS can independently re-run the verification.
You send conditions in. You get cryptographically verifiable results out.
## The primitive
Wallet auth is the OAuth-equivalent for what a wallet holds. The pattern is **read → evaluate → sign**:
1. **Read**: InsumerAPI reads the wallet's on-chain state on the requested chain
2. **Evaluate**: wallet state is checked against the supplied condition
3. **Sign**: a boolean result is returned, ES256-signed, with a `conditionHash` for tamper detection and a block anchor for replay defense
**No secrets. No identity-first. No static credentials.** Token gating is one form of condition-based access; this is the general primitive.
## Capabilities
- Single-call attestation across 37 chains: 31 EVM, Solana, XRPL, Bitcoin, Tron, Stellar, Sui
- Up to 10 conditions per request: overall `pass` is `true` only if every condition is `true`
- Ten condition types: `token_balance`, `nft_ownership` (33 of 37 chains: EVM + Solana + XRPL), `eas_attestation` (Ethereum, Optimism, Polygon, Base, Arbitrum), `farcaster_id`, `evm_view_call` (single-address-argument view function returning bool; `contractAddress`, `chainId` and `selector` required, EVM chains only), `ratio_to_amount` (EVM chains only; `contractAddress`, `chainId`, `multiple` and `amount` required; met when the balance is at least `multiple` times `amount`), `ratio_to_supply` (EVM chains, ERC-20 contracts only; `contractAddress`, `chainId` and `minFraction` required, a fraction above 0 and up to 1; met when the balance is at least that share of total supply). On v2 keys `multiple`, `amount` and `minFraction` are decimal strings, like `threshold`. `erc8004_agent` (Base; `chainId` and `agentId` required), `erc7710_delegation` (Base; `chainId`, `delegationManager`, `expectedDelegator`, `delegation` required; max 3 per call, 5-minute expiry), `account_code` (EVM chains only; `expect` required: `"none"` for a plain key account with no code, `"eip7702"` for an EIP-7702 delegation designator, `"contract"` for any other code; optional `delegate`, only with `"eip7702"`, met only when the designator points at that address; a non-EVM `chainId` or a `delegate` with another `expect` is a `400`)
- Every response is signed twice: ES256 (`sig`, `kid`) and a post-quantum ML-DSA-65 signature (`pqSig`, `pqKid`, optional fields: check them when present). `format: "jwt"` adds an ES256 JWT (`jwt`) for standard JWT-library verification, with the same claims as a post-quantum JWT (`pqJwt`)
- Optional EIP-1186 Merkle proofs (`proof: "merkle"`) for 27 of the 31 EVM chains (not ZKsync Era, Sei, Viction or XDC Network, nor any non-EVM chain): storage proofs on token_balance and ratio_to_amount conditions, account proofs (`subject: "account_code"`) on account_code conditions, plus revocation-slot proofs for erc7710_delegation on the verified v1.3.0 manager
- 30-minute attestation TTL, 5 minutes when the request includes an `erc7710_delegation` condition (`expiresAt` in response)
## Setup
Get the API key from the `insumer-auth` skill, then:
```bash
export INSUMER_API_KEY='insr_live_...'
```
## Reference values (do not hallucinate)
- **API base**: `https://api.insumermodel.com`
- **JWKS URL**: `https://insumermodel.com/.well-known/jwks.json`
- **Signing algorithm**: ES256 (ECDSA P-256)
- **Key IDs (`kid`)**: three over the same P-256 key: `insumer-attest-v2` (attest, every key issued since 2026-06-10), `insumer-trust-v2` (trust), `insumer-attest-v1` (pre-cutover keys, and the commerce discount path). **Resolve the key by the `kid` on the response; never pin one and never take `keys[0]`.** The `kid` also selects the verification rules: v1 signs bare JSON, v2 signs a domain-separated canonical preimage.
- **Auth header**: `X-API-Key: insr_live_...`
- **Attestation TTL**: 30 minutes, or 5 when the request includes an `erc7710_delegation` condition (`expiresAt` in response)
- **Signature format**: base64 P1363 (88 chars) on the `sig` field; ES256 JWT on the `jwt` field when `format: "jwt"` is requested
## Usage
### Example 1: ERC-20 balance threshold
User says: "Check if `0xd8dA...6045` holds at least 100 USDC on Base."
```bash
curl -X POST https://api.insumermodel.com/v1/attest \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"conditions": [
{
"type": "token_balance",
"contractAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"chainId": 8453,
"threshold": "100",
"label": "USDC >= 100 on Base"
}
]
}'
```
Returns `{ "ok": true, "data": { "attestation": { "pass": true|false, ... }, "sig": "...", "kid": "insumer-attest-v2", "pqSig": "...", "pqKid": "insumer-attest-pq1" }, "meta": { "creditsRemaining": ..., "creditsCharged": 1, ... } }`.
### Example 2: NFT ownership
```bash
curl -X POST https://api.insumermodel.com/v1/attest \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"conditions": [
{
"type": "nft_ownership",
"contractAddress": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D",
"chainId": 1,
"label": "Bored Ape holder"
}
]
}'
```
### Example 3: EAS attestation via compliance template
```bash
curl -X POST https://api.insumermodel.com/v1/attest \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"conditions": [
{
"type": "eas_attestation",
"template": "coinbase_verified_account",
"label": "Coinbase KYC verified"
}
]
}'
```
Available templates: see `references/condition-shapes.md` or `GET https://api.insumermodel.com/v1/compliance/templates`.
### Example 4: JWT format for standard library verification
Add `"format": "jwt"` to the request body to receive an ES256 JWT in the response (no extra cost). Verify with `jose` (Node), `PyJWT` (Python), `go-jose` (Go), etc., pointed at the JWKS URL. See the `insumer-jwks-verify` skill for the canonical recipe.
### Example 5: Merkle storage proofs (advanced)
```bash
curl -X POST https://api.insumermodel.com/v1/attest \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"proof": "merkle",
"conditions": [
{
"type": "token_balance",
"contractAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"chainId": 8453,
"threshold": "100",
"label": "USDC >= 100 on Base"
}
]
}'
```
`proof: "merkle"` returns EIP-1186 storage proofs alongside the boolean and **costs 2 credits instead of 1** (1 if no proof could be delivered). **Note**: Merkle mode reveals the raw on-chain balance to the caller. Standard mode does not. Only opt in if the consumer needs the raw balance.
Proof subjects are three: `account_balance`, `account_code` and `delegation_revocation`, plus the subject-less ERC-20 slot proof. An `account_code` condition in proof mode carries an EIP-1186 account proof with `subject: "account_code"` and the fields `blockNumber`, `nonce`, `balance`, `storageHash`, `codeHash`, `accountProof`. `codeHash` is the proven value: 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 (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. Standard mode never returns the code or the delegation target, only `met`.
### Example 6: Account code state
```bash
curl -X POST https://api.insumermodel.com/v1/attest \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"conditions": [
{ "type": "account_code", "chainId": 8453, "expect": "eip7702" }
]
}'
```
Returns `met: true` for this wallet on Base (it carries an EIP-7702 delegation designator there). The signed `evaluatedCondition` is `{"type":"account_code","chainId":8453,"expect":"eip7702","operator":"code_state"}`. Add `"delegate": "0x…"` to require a specific delegation target; it is echoed, lowercase, inside the signed `evaluatedCondition`. See `references/condition-shapes.md`.
### Example 7: XRPL trust line
```bash
curl -X POST https://api.insumermodel.com/v1/attest \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"xrplWallet": "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH",
"conditions": [
{
"type": "token_balance",
"contractAddress": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De",
"chainId": "xrpl",
"currency": "RLUSD",
"threshold": "50",
"label": "RLUSD >= 50 on XRPL"
}
]
}'
```
XRPL conditions use `xrplWallet` instead of `wallet`. A trust-line token also needs a `currency` field; native XRP and NFTs do not. See `references/chains.md` for the full chain coverage table.
## Code emission rules
When emitting integration code that calls `/v1/attest`, the agent MUST:
1. **Read the API key from an env var.** Never inline `insr_live_...`.
2. **Verify the signature offline.** Pair this skill with `insumer-jwks-verify`. The signed boolean is the product: the JSON body alone is untrusted.
3. **Do not send `decimals`.** It is optional. Leave it out: the token's own decimals are always read from the chain. If sent it is only a cross-check: on EVM chains, Tron and Sui a value that differs from the token's own decimals is rejected with a `400`.
3a. **Send the `token_balance` `threshold` as a decimal string** (`"100"`, not `100`). Keys created from 2026-06-10 sign with `kid: insumer-attest-v2` and reject a JSON number with a `400`; a string is accepted by both v1 and v2 keys.
4. **Call from a backend, not a browser.** The API key is a backend credential.
5. **Don't cache the verdict.** Cache the JWKS (the `jose` library's `createRemoteJWKSet` does this for you). Pass/fail expires at `expiresAt` (30 minutes, or 5 for an `erc7710_delegation` condition), and wallet state changes.
6. **Don't request `proof: "merkle"` unless the caller needs the raw balance.** Standard mode is boolean-not-balance by construction; Merkle mode opts out of that privacy property.
## Helper script
`scripts/attest.py`: Python helper that wraps `POST /v1/attest`. Reads `INSUMER_API_KEY` from env, accepts the full JSON request body (wallet + conditions) on stdin or via `--body-file`, prints the signed response.
```bash
echo '{"wallet":"0x...","conditions":[{"type":"token_balance",...}]}' | python scripts/attest.py
```
## Error handling
| Status | Cause | Fix |
|---|---|---|
| `400` | Missing/invalid wallet, conditions, or condition fields | Check request body against `references/condition-shapes.md` |
| `401` | Invalid API key | See `insumer-auth` skill |
| `402` | With a key: out of verification credits. With no key at all: a pay-per-call quote, not a credit problem | With a key, top up via Path 4 in `insumer-auth`, only after the user approves the payment. With no key, set `INSUMER_API_KEY` and call again |
| `429` | Daily request limit reached | It resets on a timer, not by slowing down: wait the number of seconds in the `Retry-After` header |
| `503` | Upstream blockchain data source unavailable | Retryable after a short delay; no credits charged |
A `503` carries error code `rpc_failure`. It means a read did not complete and nothing was signed. It is never a `false`: do not report it as a condition that was not met. Retry the call.
## Related skills
| Skill | Purpose |
|---|---|
| `insumer-auth` | Get a free API key, configure env vars |
| `insumer-jwks-verify` | Offline ES256 verification of the response |
| `insumer-trust` | Curated multi-dimensional wallet profile (alternative to custom conditions) |
## References
- [chains.md](references/chains.md): full 37-chain coverage table
- [condition-shapes.md](references/condition-shapes.md): every supported condition type with full request shape
- [InsumerAPI OpenAPI spec](https://insumermodel.com/openapi.yaml)
- [Public JWKS](https://insumermodel.com/.well-known/jwks.json)
Referenced files: 3 — file references are metadata, not package contents.
insumer-auth18.4 KB
View saved version →
---
name: insumer-auth
description: >
InsumerAPI wallet auth: get an InsumerAPI key, configure INSUMER_API_KEY,
check or top up InsumerAPI credits, or debug InsumerAPI 401/402/429 errors.
Covers four key paths: free (email), paid (Stripe), an agent's first key
paid on-chain, and an on-chain top-up of an existing key. The on-chain paths
need the user's explicit approval of every payment.
metadata:
version: "0.2.3"
author: InsumerAPI
---
# InsumerAPI Authentication
Wallet auth via InsumerAPI starts with an API key. The key authenticates the *caller*; the signing key that produces the boolean response stays with the issuer and never leaves. **Read → evaluate → sign.**
This skill covers **four** ways to get or extend a key. Pick the one that matches the caller and stop: don't push upsell copy into emitted code.
## Pick the right path
| Who's getting the key? | What they want | Path | Endpoint |
| ----- | ----- | ----- | ----- |
| **Human**, building or testing | Free starter access (10 free verifications plus 100 requests a day) | **Path 1: Free** | `POST /v1/keys/create` |
| **Human**, wants Pro/Enterprise tier | Higher limits, bulk credits, monthly billing | **Path 2: Paid (Stripe)** | <https://insumermodel.com/pricing/> for a new key; <https://insumermodel.com/developers/account/> to upgrade an existing one |
| **Agent** whose operator funds it, **first key** | Bootstrap with no email, sender wallet = identity | **Path 3: Agent onboarding (crypto)** | `POST /v1/keys/buy` |
| **Agent or human**, **already has a key** | Top up credits, keep the key/history/integrations | **Path 4: Top-up (crypto)** | `POST /v1/credits/buy` |
**Decision rule:** default to Path 1 (free). Use Path 3 or Path 4 only when the user, or the operator of an autonomous agent, has explicitly chosen to pay with crypto. Path 4 tops the key up in place: it keeps its history, tier, and integrations.
**Pay per call, with no key at all:** `POST /v1/attest`, `/v1/trust` and `/v1/trust/batch` accept x402. Called with no credential headers they answer `402 Payment Required` with a quote, not an error about credits. Pay the quote in USDC on Base, Polygon, Arbitrum, Arc or Solana and retry with the `PAYMENT-SIGNATURE` header; the quote carries the exact amount. The payer is charged only for a successful answer, and the payer sees the answer only after the payment settled. If a paid call answers `503`, retry the exact same request with the same payment header and do not sign a new authorization: on Arc the payment may still be settling, and the retry resolves to the same payment. Like the on-chain paths, it needs the user's explicit approval of the payment. The skills in this repo call with a key.
**Payments need explicit approval (never violate).** Paths 3 and 4 start with a real, irreversible transfer of funds. Never send, or prepare for signing, any payment unless the user has confirmed the amount, token, chain and recipient address in this session. This skill documents the calls; it does not authorize a payment.
## Reference values (do not hallucinate)
- **API base**: `https://api.insumermodel.com`
- **Auth header**: `X-API-Key: insr_live_...`
- **Purchases are paid in USDC only.** Payments on Bitcoin or Tron, and USDT or any other token, are refused with a `400`
- **Platform wallet, USDC on Ethereum, Base, Polygon, Arbitrum, Optimism or Avalanche**: `0xee432065474D2C852700f74e3fD81e2Bcc16a3D4`
- **Platform wallet, USDC on BNB Chain**: `0xAd982CB19aCCa2923Df8F687C0614a7700255a23`. This previous address also still takes USDC on the other EVM chains during the changeover; it takes no USDT
- **Platform wallet, USDC on Solana**: `8fyEGM5tRjMocKQWaVXyM2XvvnTxR3sWdrYtug2zeLX4`. The previous Solana address `6a1mLjefhvSJX1sEX8PTnionbE9DqoYjU6F6bNkT4Ydr` still takes USDC during the changeover; it takes no USDT
- **Volume discounts** (paths 3 and 4): $5–$99 → 25 credits per $1 ($0.04/credit), $100–$499 → 33 credits per $1 (about $0.03/credit), $500+ → 50 credits per $1 ($0.02/credit). The price is per credit, not per call: an attestation costs 1 credit and a trust profile 3 (2 and 6 with `proof: "merkle"`)
- **Supported payment chains (these eight and no others)**: USDC on Ethereum (1), Base (8453), Polygon (137), Arbitrum (42161), Optimism (10), BNB Chain (56), Avalanche (43114) or Solana (`"solana"`). The other chains the API can read, Bitcoin and Tron included, are NOT payment chains, and funds sent on a chain outside this list cannot be recovered
- **Sender signature** (Path 3 always; Path 4 when the payment registers a wallet to the key): the wallet that sent the payment signs a fixed five-line text naming the payment, and the signature goes in `senderSignature`. See [Sign the sender text](#sign-the-sender-text)
---
## Path 1: Free key (human, no payment)
Free tier: 10 free verifications plus 100 requests a day, no signup beyond an email.
```bash
curl -s -X POST https://api.insumermodel.com/v1/keys/create \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","appName":"insumer-agent-skills","tier":"free"}'
```
Default `appName` to `"insumer-agent-skills"` unless the user names their app. It is a label on the key that tells InsumerAPI which channel the key came from.
Response:
```json
{
"success": true,
"key": "insr_live_...",
"name": "insumer-agent-skills",
"tier": "free",
"dailyLimit": 100,
"apiKeyCredits": 10
}
```
Constraints:
- One free key per email (returns `409` if a free key already exists)
- Maximum 3 keys per IP per 24 hours (returns `429` on exceeded)
- Free tier only: paid tiers go through Path 2
Tell the user to set:
```bash
export INSUMER_API_KEY='insr_live_...'
```
---
## Path 2: Paid first key (human, Stripe checkout)
Pro and Enterprise tiers (higher daily limits, bulk credits, monthly billing) go through Stripe checkout. A new paid key starts from <https://insumermodel.com/pricing/>. To upgrade an existing key, paste it at <https://insumermodel.com/developers/account/>: the upgrade applies to that key and keeps it.
Use this path **only** when the user explicitly asks about paid tiers, SLAs, monthly billing, or wants a higher-tier key from scratch. Don't recommend it as a default.
---
## Path 3: Agent first key (crypto, no email)
For an agent whose operator has chosen to fund it. After the user approves the payment, USDC goes to the platform wallet, then the agent calls `POST /v1/keys/buy` with the transaction hash and the paying wallet's signature. **No email or prior authentication needed**: the sender wallet address from the transaction becomes the key's identity, and that wallet signs a short text to confirm it sent the payment.
### Step 1: Send payment
Send the desired amount (minimum $5) of USDC to the platform wallet for that chain, on one of the eight supported payment chains listed above. The address is the literal `recipient`.
### Step 2: Sign the sender text
The wallet that sent the payment signs the text in [Sign the sender text](#sign-the-sender-text), with `Purpose: key purchase`. To get the exact text, call Step 3 without `senderSignature`: the `400` carries it, and the payment is not used.
### Step 3: Call `/v1/keys/buy` with the transaction hash and the signature
```bash
curl -s -X POST https://api.insumermodel.com/v1/keys/buy \
-H "Content-Type: application/json" \
-d '{
"txHash": "0xabc...",
"chainId": 8453,
"amount": 10,
"senderSignature": "0x...",
"appName": "insumer-agent-skills",
"keyDelivery": "apiKey"
}'
```
Default `appName` to `"insumer-agent-skills"` as in Path 1, unless the user names their app.
Fields:
- `txHash`: the transaction proving payment
- `chainId`: the chain the payment was sent on: the numeric chain ID for EVM, or `"solana"`
- `appName`: required. Name for the new key (the skills send `"insumer-agent-skills"`)
- `amount`: required. USDC amount sent (minimum 5)
- `senderSignature`: required. The paying wallet's signature over the sender text (see [Sign the sender text](#sign-the-sender-text))
- `keyDelivery`: optional. Send `"apiKey"` to receive the key string. With the default, `"wallet"`, an EVM payment that issues an Insumer Access pass to the paying wallet returns **no key**: that wallet authenticates with `Authorization: Wallet` instead (see `authHint` in the response). Send `"apiKey"` whenever the key string is needed.
Response:
```json
{
"ok": true,
"data": {
"success": true,
"key": "insr_live_...",
"name": "insumer-agent-skills",
"tier": "paid",
"dailyLimit": 10000,
"creditsAdded": 250,
"totalCredits": 250,
"effectiveRate": "$0.0400/credit",
"chainName": "Base",
"registeredWallet": "0x...",
"passMint": { "status": "minted", ... },
"authMethod": "both",
"authHint": "...",
"usdcPaid": "10.00"
}
}
```
The agent stores `data.key` in its secret store; that's its persistent identity going forward. One key per sender wallet address (returns `409` if the wallet already has a key).
**Important:** crypto sent on unsupported chains or to the wrong address cannot be recovered. All purchases are final.
---
## Path 4: Top up existing key (crypto, agent or human)
When an existing key (free, paid, or agent-onboarded) runs low on credits, top it up with a payment the user has approved to the platform wallet, then call `POST /v1/credits/buy`. **The key keeps its history, tier, and integrations**: credits just increment.
### Step 1: Send payment to the platform wallet
Same wallets and chains as Path 3: USDC only.
### Step 2: Call `/v1/credits/buy`
```bash
curl -s -X POST https://api.insumermodel.com/v1/credits/buy \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"txHash": "0xabc...",
"chainId": 8453,
"amount": 10
}'
```
Required fields:
- `txHash`: the transaction proving payment
- `chainId`: the chain the payment was sent on: the numeric chain ID for EVM, or `"solana"`
- `amount`: USDC amount sent (minimum 5)
Sometimes required:
- `senderSignature`: the paying wallet's signature over the sender text, with `Purpose: credit purchase` (see [Sign the sender text](#sign-the-sender-text)). Required when the key has no registered wallet yet, and with `"updateWallet": true` from a different wallet. A top-up from the wallet already registered needs none.
Sender verification: the **first** top-up registers the sender wallet to the key, and that wallet signs the sender text. Subsequent top-ups must come from the **same** sender. To replace the registered wallet, send from the new wallet and include `"updateWallet": true` and a `senderSignature` from the new wallet.
Response:
```json
{
"ok": true,
"data": {
"creditsAdded": 250,
"totalCredits": 260,
"usdcPaid": "10.00",
"effectiveRate": "$0.0400/credit",
"chainName": "Base"
}
}
```
This path keeps the same key. Mention it when the user asks how an agent can pay for its own access.
---
## Sign the sender text
A transaction hash is public, so the API asks the wallet that sent the payment to sign a fixed text naming it. The API builds the text and checks the signature against the sender it read from the chain, never against an address the caller states. The text is five lines joined by `\n`, with no trailing newline:
```
api.insumermodel.com
Confirm the wallet that sent this payment.
Purpose: key purchase
Chain: eip155:8453
Transaction: 0xabc...
```
- `Purpose`: `key purchase` for `/v1/keys/buy`, `credit purchase` for `/v1/credits/buy`
- `Chain`: `eip155:<chainId>` for an EVM chain (for example `eip155:8453` for Base), or `solana`
- `Transaction`: the transaction id. EVM: `0x` plus the hash in lowercase hex. Solana: the signature as given
The simplest way to get the text exactly right: send the request without `senderSignature`. The API answers `400` with the exact text after a blank line, and the payment is not used. `scripts/buy_key.py` and `scripts/buy_credits.py` print it for you.
How to sign:
- **EVM**: EIP-191 `personal_sign` over the text (the wallet's "sign message"). Send the `0x` hex signature. Smart-contract wallets are accepted (ERC-1271)
- **Solana**: the wallet's `signMessage` over the UTF-8 text. Send the signature in base58 or base64
The user signs with their own wallet tooling; these skills never sign. Signing moves no funds, but it is the user's wallet: show the user the exact text before it is signed.
---
## Verify auth works
After setting `INSUMER_API_KEY`, confirm with:
```bash
curl -s https://api.insumermodel.com/v1/credits \
-H "X-API-Key: $INSUMER_API_KEY"
```
Expected: `{"ok":true,"data":{"apiKeyCredits":<n>,"tier":"...","dailyLimit":<n>}}`. If you see this, auth is working and you're ready for the other skills.
## Common workflows
### Initial setup (human)
1. Run Path 1 `curl` with the user's email (and `appName`, default `"insumer-agent-skills"`)
2. User exports `INSUMER_API_KEY=...`
3. Verify with `GET /v1/credits`
### Agent setup from cold start (operator-funded agent)
1. The operator approves the payment: amount, token, chain and recipient (for example $10 USDC on Base to `0xee432065474D2C852700f74e3fD81e2Bcc16a3D4`)
2. The payment is sent, and the paying wallet signs the sender text (`Purpose: key purchase`)
3. The agent calls Path 3 (`POST /v1/keys/buy`) with the txHash, `amount`, `senderSignature` and `"keyDelivery": "apiKey"`
4. Agent stores the returned `key` to its persistent secret store
### Agent runs out of credits mid-task
1. The operator approves a top-up, sent **from the same sender wallet that bought the original key**
2. Agent calls Path 4 (`POST /v1/credits/buy`) with the txHash and amount, using its existing key (no `senderSignature` needed from the registered wallet)
3. Credits increment; key, history, and tier preserved
### Check credit balance
```bash
curl -s https://api.insumermodel.com/v1/credits \
-H "X-API-Key: $INSUMER_API_KEY"
```
## Error handling
| Status | Endpoint | Cause | Fix |
|---|---|---|---|
| `400` | any | Missing required fields | Check request body |
| `400` | `/v1/keys/buy`, `/v1/credits/buy` | Not paid in USDC on an accepted chain: Bitcoin, Tron, USDT, another token, or another `chainId` | Purchases are USDC only, on the eight payment chains above |
| `400` | `/v1/keys/buy`, `/v1/credits/buy` | `senderSignature` missing or not a string. The message carries the exact text to sign | Have the paying wallet sign that text, then send the same request with `senderSignature`. The payment has not been used |
| `401` | any auth'd | `X-API-Key` missing or invalid (on the three endpoints in the next row, a request with no key at all gets a `402` instead) | Verify env var, check spelling |
| `402` | `/v1/attest`, `/v1/trust`, `/v1/trust/batch` | With a key: out of credits. With no key at all: a pay-per-call quote, not a credit problem | With a key, Path 4 top-up (with approval) or Path 2 (human). With no key, set `INSUMER_API_KEY` and call again |
| `403` | `/v1/keys/buy`, `/v1/credits/buy` | `senderSignature` does not match the wallet that sent the payment. The message repeats the text to sign | Sign that exact text with the wallet that sent the payment, and retry |
| `403` | `/v1/credits/buy` | Payment sender is not the wallet registered to this key | Send from the registered wallet, or add `"updateWallet": true` and a `senderSignature` from the new wallet |
| `409` | `/v1/keys/create` | Free key already exists for this email | Reuse existing key |
| `409` | `/v1/keys/buy` | Wallet already has a key, or txHash already used | Wallet already has a key: top up via Path 4 instead (the same transaction can be used). txHash already used: that payment is spent, a new payment is needed |
| `422` | `/v1/keys/buy`, `/v1/credits/buy` | On-chain verification failed | Wait for confirmations, verify chain & amount |
| `422` | `/v1/keys/buy`, `/v1/credits/buy` | The wallet that sent the payment could not be determined, so the payment cannot be bound to a key | Send the next payment as a plain USDC transfer from one wallet. This payment cannot be used for a key or credits. |
| `429` | `/v1/keys/create` | 3 keys per IP per 24h, or free-key creation paused for the day | Wait 24h |
| `503` | `/v1/keys/buy`, `/v1/credits/buy` | The chain read, or a smart-wallet (ERC-1271) signature check, could not be completed. The payment has not been used | Retry the same request in a few seconds |
| `429` | any keyed endpoint | Daily request limit (100/day on free) | Wait for reset (`Retry-After`), or upgrade tier |
## Key hygiene (never violate)
- **Backend only.** `insr_live_...` is a long-lived backend credential. Never inline in source. Never expose in browser JS, `NEXT_PUBLIC_*`, `VITE_*`, `REACT_APP_*`, localStorage, or logs.
- **Env var pattern.** Always read from `process.env.INSUMER_API_KEY` / `os.environ["INSUMER_API_KEY"]`. Never hardcode in source files.
- **Don't echo the key in error messages.** Log "INSUMER_API_KEY not set" or "auth failed", not the key value or its suffix.
- **Don't commit `.env` files** containing the key. Use `.env.example` for the variable name only.
## Helper script
`scripts/create_key.py`: Python helper for Path 1. Reads `--email`, POSTs to `/v1/keys/create` with `appName: "insumer-agent-skills"` (override with `--app-name`), prints the key and a `.env` snippet.
```bash
python scripts/create_key.py --email you@example.com
```
For Path 3 (`/v1/keys/buy`) and Path 4 (`/v1/credits/buy`), the payment itself is made with the user's own wallet tooling, after their approval; these skills never sign or send a transaction, and never sign the sender text. To submit the resulting transaction hash, the `curl` shapes above are sufficient, or use `scripts/buy_key.py` and `scripts/buy_credits.py`. Run either one without `--sender-signature` when a signature is needed: it prints the exact text to sign and how to sign it, and the payment is not used. Then run the same command again with `--sender-signature <signature>`.
```bash
python scripts/buy_key.py --tx 0xabc... --chain 8453 --amount 10
python scripts/buy_key.py --tx 0xabc... --chain 8453 --amount 10 --sender-signature 0x...
```
## Related skills
| Skill | Purpose |
|---|---|
| `insumer-attest` | Single-call wallet condition attestation |
| `insumer-trust` | Multi-dimensional wallet trust profile |
| `insumer-trust-batch` | Batch trust profiles for multiple wallets |
| `insumer-jwks-verify` | Offline ES256 verification of returned JWTs |
## References
- [Pricing & supported payment chains](https://insumermodel.com/pricing/)
- [Developer account portal (Stripe checkout)](https://insumermodel.com/developers/account/)
- [OpenAPI spec](https://insumermodel.com/openapi.yaml)
- [Public JWKS](https://insumermodel.com/.well-known/jwks.json)
Referenced files: 3 — file references are metadata, not package contents.
insumer-discount8.46 KB
View saved version →
---
name: insumer-discount
description: >
Token-holder discounts at InsumerAPI merchants: read a store's terms, check
what a wallet qualifies for (free), optionally prove control of the wallet
by signing an EIP-4361 message, and create a signed single-use discount code
(INSR-XXXXX). Use when the user or agent wants a holder discount at a store
in the InsumerAPI merchant directory, wants to know whether signing with the
wallet gets a bigger discount, or needs a discount in ACP or UCP checkout
format.
metadata:
version: "0.1.1"
author: InsumerAPI
---
# InsumerAPI Token-Holder Discounts
A store lists the tokens and NFTs it recognizes and the discount each earns. InsumerAPI reads the wallet, works out the discount, and issues a signed, single-use code (`INSR-XXXXX`, valid 30 minutes) that the store redeems. The store sees the code and the percentage, never the wallet or its balances.
**Two routes, the store sets the terms.**
- **Proven wallet:** the caller sends `walletProof`, a message signed by the wallet. The wallet gets the full discount its holdings earn (under the store's tiers and cap), without the unproven-wallet daily limit.
- **Unproven wallet:** no proof. The store decides what that gets: the same discount (the default), up to a lower percentage, or nothing, and optionally a daily limit per wallet.
Read the terms first, then decide whether signing is worth it.
## Setup
Get the API key from the `insumer-auth` skill, then:
```bash
export INSUMER_API_KEY='insr_live_...'
```
A code that carries a discount uses one credit from the API key that owns the store. A caller using another key is not charged; when the caller's key owns the store, the caller's key pays. A 0% result is free.
## Reference values (do not hallucinate)
- **API base**: `https://api.insumermodel.com`
- **Store terms**: `GET /v1/merchants/{merchantId}` → `data.walletTerms` (no key needed). The free check returns the same `walletTerms`, and also answers for a store that is not listed in the public directory
- **Free check**: `GET /v1/discount/check?merchant={merchantId}&wallet={0x...}` (no key needed)
- **Create a code**: `POST /v1/verify` with `X-API-Key` (also `POST /v1/acp/discount` and `POST /v1/ucp/discount` for checkout formats)
- **Validate a code**: `GET /v1/codes/{code}` (no key needed)
- **Proof message domain**: `api.insumermodel.com`
- **Proof message URI**: `https://api.insumermodel.com/v1/merchants/{merchantId}` (exactly this merchant)
- **Proof freshness**: signed within the last 5 minutes; every nonce used once
- **Proof wallets**: EVM wallets that sign with `personal_sign` (EIP-191). Smart-contract wallets are not accepted yet. Solana and XRPL wallets can still get the unproven terms.
## Usage
### Step 1: read the store's terms (free)
```bash
curl -s https://api.insumermodel.com/v1/merchants/acme-store
```
```json
"walletTerms": {
"proofAccepted": ["evm"],
"proven": { "maxDiscountsPerWalletPerDay": null },
"unproven": { "maxDiscount": 5, "maxDiscountsPerWalletPerDay": 10 }
}
```
`unproven.maxDiscount`: `null` = the same as proven, `0` = no discount without proof, otherwise at most that percent. `unproven.maxDiscountsPerWalletPerDay`: `null` = no limit.
### Step 2: check what the wallet qualifies for (free)
```bash
curl -s "https://api.insumermodel.com/v1/discount/check?merchant=acme-store&wallet=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
```
`totalDiscount` is what the wallet gets **without** proof. If `discountIfProven` is present, signing gets that instead. If it is absent, signing changes the discount by nothing (it still lifts the daily limit).
### Step 3 (optional): prove the wallet
Sign this exact message with the wallet, using `personal_sign`:
```
api.insumermodel.com wants you to sign in with your Ethereum account:
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
Prove wallet for a discount.
URI: https://api.insumermodel.com/v1/merchants/acme-store
Version: 1
Chain ID: 1
Nonce: k3j9x2m8q1
Issued At: 2026-10-05T18:00:00.000Z
```
Line 2 is the wallet (checksummed or lowercase), the URI names the merchant, the nonce is fresh random letters and digits (8 or more), and `Issued At` is the current time in ISO 8601. `scripts/proof_message.py` builds it for you.
### Step 4: create the code
```bash
curl -X POST https://api.insumermodel.com/v1/verify \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"merchantId": "acme-store",
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"walletProof": { "message": "<the message, exactly as signed>", "signature": "0x..." }
}'
```
Leave out `walletProof` for the unproven route. The response carries `code`, `totalDiscount`, `walletProven`, and `discountIfProven` when proof would have got more. `GET /v1/codes/{code}` reports the code's percentage, expiry and `walletProven`.
## Code emission rules
When emitting integration code for this flow, the agent MUST:
1. **Read the terms or run the free check before creating a code.** Creating a code that carries a discount spends a credit from the API key that owns the store; a 0% result is free.
2. **Never hold the user's private key to sign for them.** The wallet's owner signs, in their own wallet. An agent signs only with a wallet it controls itself.
3. **Build a fresh message for every request.** A nonce is accepted once, and a message older than 5 minutes is refused.
4. **Send the proof with `wallet` only.** A proof sent with `solanaWallet` or `xrplWallet` is a `400`.
5. **Treat a failed proof as a failed proof.** A `401` on a request with `walletProof` means the proof was refused (wrong wallet, merchant, domain, expired, nonce reused or bad signature); no credit was used. Fix and re-sign, or retry without proof to take the unproven terms. It never silently falls back.
6. **Read the API key from an env var**, and call from a backend.
## Helper scripts
`scripts/proof_message.py`: prints the EIP-4361 message for a wallet and merchant, with a fresh nonce and the current time. It never signs and never handles a private key: the wallet signs the printed message itself, and you save `{ "message": ..., "signature": ... }` as the proof file.
```bash
python scripts/proof_message.py --merchant acme-store --wallet 0xd8dA...6045
```
`scripts/discount.py`: reads the terms, runs the free check, and with `--create` creates the code (adding `--proof-file proof.json` for the proven route).
```bash
python scripts/discount.py --merchant acme-store --wallet 0xd8dA...6045
python scripts/discount.py --merchant acme-store --wallet 0xd8dA...6045 --create --proof-file proof.json
```
## Error handling
| Status | Cause | Fix |
|---|---|---|
| `400` | Missing or invalid field; proof sent with a non-EVM wallet | Check the request body |
| `401` | Missing API key, or a `walletProof` that was refused | See `insumer-auth`; re-sign a fresh message |
| `402` | The API key that owns the store has no credits left | If the store is the caller's own, top up (Path 4 in `insumer-auth`). Otherwise nothing the caller can do; tell the user |
| `403` | The store has not enabled app access, or has no active owner key | Nothing the caller can do |
| `404` | Merchant not found. `GET /v1/merchants/{merchantId}` also answers `404` for a store that is paused or not listed in the public directory | Check the merchant ID, then run the free check, which answers for those stores |
| `409` | The store's recognition is paused by its owner | No code is created and no credit is used; tell the user |
| `429` | The key's daily limit; the store's daily limit for an unproven wallet; or the hourly limit on discount codes requested with a key other than the store owner's | For the store's daily limit, prove the wallet (the limit does not apply to a proven wallet) or try after 00:00 UTC. For the hourly limit, wait the time the message gives: proving the wallet does not lift it |
| `503` | A read did not complete (`rpc_failure`) | Retry after a short delay; no credit used. With a `walletProof`, sign a fresh message first: the nonce in the refused request has been used |
A `503` with `rpc_failure` is never "not eligible". Retry the call, with a freshly signed message if the request carried a `walletProof`.
## Related skills
| Skill | Purpose |
|---|---|
| `insumer-auth` | Get a free API key, configure env vars |
| `insumer-jwks-verify` | Verify signed responses offline |
| `insumer-attest` | Signed yes/no on any wallet condition |
## References
- [InsumerAPI OpenAPI spec](https://insumermodel.com/openapi.yaml)
- [EIP-4361: Sign-In with Ethereum](https://eips.ethereum.org/EIPS/eip-4361)
Referenced files: 2 — file references are metadata, not package contents.
insumer-hosted-tools5.24 KB
View saved version →
---
name: insumer-hosted-tools
description: >
How to use the InsumerAPI MCP tools this plugin connects to (the hosted
server at api.insumermodel.com/mcp, no key needed). Use when the user asks
to check if a wallet holds a token or an amount, whether a wallet owns an
NFT, whether a wallet is a registered ERC-8004 agent, for a wallet trust
profile, for InsumerAPI's signing keys or how to verify an attestation, for
compliance templates, or for a merchant's discount, and the insumer MCP
tools are available. Do not use for exact balances, prices, or sending
transactions. Explains the shared daily allowance and when to switch to the
REST skills with the user's own key.
metadata:
version: "0.1.1"
author: InsumerAPI
---
# InsumerAPI hosted tools
This plugin connects to InsumerAPI's hosted MCP server. The tools need no account and no key. Prefer them over the REST path in the other skills whenever they are available; the REST skills remain the reference for request shapes, response fields and verification.
**Boolean, not balance.** Every signed result says whether a wallet satisfies a condition right now. It never says how much the wallet holds.
## The ten tools
| Tool | What it does | Spends the allowance? |
| ---- | ------------ | --------------------- |
| `insumer_jwks` | The public signing keys: an ECDSA P-256 key under its kids, then the ML-DSA-65 post-quantum key | No |
| `insumer_attest` | Signed yes or no for 1 to 10 conditions on one wallet, across 37 chains | Yes |
| `insumer_compliance_templates` | Named EAS templates (Coinbase Verifications, Gitcoin Passport) usable as conditions | No |
| `insumer_wallet_trust` | Signed trust profile for one EVM wallet: 155 base checks across 27 chains in ten dimensions, held or not held, no score | Yes |
| `insumer_batch_wallet_trust` | Trust profiles for up to 10 wallets in one call | Yes, per wallet |
| `insumer_list_merchants` | The public merchant directory | No |
| `insumer_get_merchant` | One merchant's public profile: token tiers, NFT collections, discount mode | No |
| `insumer_list_tokens` | Registered tokens and NFT collections | No |
| `insumer_check_discount` | What discount a wallet would get at a merchant | No |
| `insumer_validate_code` | Whether an INSR-XXXXX discount code is valid | No |
## Rules for calling them
- **Quantities are decimal strings in display units.** `threshold: "100"` means 100 tokens, not base units. Never send `decimals`; the token's own decimals are read from the chain.
- **Chains.** EVM chains by numeric chain ID (1 Ethereum, 8453 Base, 137 Polygon, 42161 Arbitrum, 10 Optimism, 56 BNB Chain, and so on). Non-EVM by name: `solana`, `xrpl`, `bitcoin`, `tron`, `stellar`, `sui`. Each non-EVM chain needs its own wallet field (`solanaWallet`, `xrplWallet`, `bitcoinWallet`, `tronWallet`, `stellarWallet`, `suiWallet`).
- **Native coins.** `contractAddress: "native"` on every chain except Sui, where native SUI is the coin type `0x2::sui::SUI`.
- **NFTs** need the NFT contract address; `native` with `nft_ownership` is rejected.
- **Read the result, not the request.** Report `met` per condition and the overall `pass`. Quote the attestation ID, the `kid` and `pqKid`, and the block or ledger it was read at. Do not invent a balance; none is returned.
- **Verification.** Anyone can check a result offline with the `insumer-verify` package (npm and PyPI) against the keys from `insumer_jwks`. Match keys by `kid`, never by position. The `insumer-jwks-verify` skill has the details.
## The shared allowance
The hosted server runs on one shared allowance for the tools that produce signed results (`insumer_attest`, `insumer_wallet_trust`, `insumer_batch_wallet_trust`). When it is used up for the day, those tools return an error that says so; it resets at 00:00 UTC. The free tools keep working.
When that happens, or when the user needs more than occasional calls, key and credit management, or the merchant tools:
1. Tell the user plainly that the shared allowance is used up for today.
2. Offer the user's own free key: the `insumer-auth` skill creates one with an email address (10 verifications plus 100 requests a day, no card), and the `insumer-attest`, `insumer-trust` and `insumer-trust-batch` skills call the REST API with it.
3. Never ask the user to paste an API key into the chat for use with the hosted tools. The hosted tools take no key. A key belongs in the user's own environment, as the REST skills describe.
## What not to claim
- InsumerAPI does not score, rank or vet wallets. A trust profile lists what is held and what is not.
- Registration as an ERC-8004 agent is permissionless minting; a `met: true` there means registered, not vetted.
- A signed `false` is a verdict, not an error. An error is a response with `ok: false`.
- InsumerAPI stores no wallet addresses or conditions from verification requests. Say so only in those words; do not expand on how the service works internally.
## Related skills
- `insumer-attest`, `insumer-trust`, `insumer-trust-batch`: the same operations over REST with the user's own key, with the full request and response shapes.
- `insumer-auth`: getting and topping up a key.
- `insumer-discount`: a signed discount code at an InsumerAPI merchant, over REST with the user's own key.
- `insumer-jwks-verify`: verifying any signed response offline.
insumer-jwks-verify14.5 KB
View saved version →
---
name: insumer-jwks-verify
description: >
Offline ES256 verification of InsumerAPI signed responses against the public
JWKS. Use when the user receives a signed attestation from /v1/attest,
/v1/trust, or /v1/trust/batch and needs to verify the signature without
trusting the JSON body. Covers both the JWT path (jose / PyJWT / go-jose) and
the raw sig path (ES256 over the canonical attestation/trust object).
metadata:
version: "0.2.1"
author: InsumerAPI
---
# InsumerAPI Offline JWKS Verification
The signed boolean is the product. The JSON body alone is untrusted: anyone can fabricate a JSON response. **Verify the signature, every time.**
InsumerAPI signs every `/v1/attest`, `/v1/trust`, and `/v1/trust/batch` response twice: with ES256 (ECDSA P-256) and with a post-quantum ML-DSA-65 signature. The public keys are published as a standard JWKS at `https://insumermodel.com/.well-known/jwks.json`. The signing key never leaves the issuer; anyone holding the JWKS can independently re-run the verification, with no callback to InsumerAPI.
## Reference values (do not hallucinate)
- **JWKS URL**: `https://insumermodel.com/.well-known/jwks.json`
- **Algorithm**: ES256 (ECDSA P-256)
- **Key IDs (`kid`)**: five JWKS entries over two keys. Three EC kids on the same P-256 key: `insumer-attest-v2` (attest, every key issued since 2026-06-10), `insumer-trust-v2` (trust), `insumer-attest-v1` (pre-cutover keys, and the commerce discount path). Then two RFC 9964 `AKP` entries for the ML-DSA-65 post-quantum key: `insumer-attest-pq1` and `insumer-trust-pq1`, selected by the response `pqKid`. Resolve by kid, never by position; fail closed on an unknown kid.
- **JWT issuer claim** (when `format: "jwt"` is requested): `https://api.insumermodel.com`
- **Raw signature format**: base64 P1363 (88 chars) on the `sig` field
## Two verification paths
InsumerAPI returns up to **two** verifiable forms in a signed response:
1. **`sig` field** (every response): base64 P1363 ES256 signature over the preimage the `kid` selects (see "What's signed" in `references/jwks-format.md`). Attestations: for `insumer-attest-v2`, the domain tag `insumer.attestation.v2` + newline + canonical JSON (keys sorted recursively, no whitespace) of `{v:2, id, pass, results, attestedAt}`; for `insumer-attest-v1`, `JSON.stringify({id, pass, results, attestedAt})` in that insertion order. Trust profiles: for `insumer-trust-v2`, the domain tag `insumer.trust.v2` + newline + canonical JSON of the whole `trust` object; for `insumer-attest-v1`, `JSON.stringify(trust)` as issued. Verify with any ES256 library + the JWKS key the `kid` names. Responses also carry `pqSig`/`pqKid`, a post-quantum ML-DSA-65 signature over the same preimage under a post-quantum domain tag (spec Check 6). The two fields are optional: a stored artifact without them reads as `absent`, which is not a failed signature.
2. **`jwt` field** *(`/v1/attest` only, and only when `"format": "jwt"` is in the request body; `/v1/trust` and `/v1/trust/batch` have no JWT form)*: standard ES256 JWT carrying the attestation as claims, beside `pqJwt`, the same claims under the post-quantum signature. Verify with any standard JWT library pointed at the JWKS URL.
The `jwt` path is easier when the consumer is already using a JWT library, and it signs the wallet as the `sub` claim. The raw `sig` path is more compact, but for most condition types it does not sign the wallet: it proves that some wallet met the condition, not which one. Use the JWT form (or an `erc8004_agent` / `erc7710_delegation` condition, which carry the wallet in the signed result) whenever the relying party must know which wallet was attested.
## Recipe 1: JWT verification (Node.js, `jose`)
Add `"format": "jwt"` to the `/v1/attest` request body (trust profiles have no JWT form; use Recipe 3), then:
```javascript
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://insumermodel.com/.well-known/jwks.json')
);
async function verifyAttestation(jwtString) {
const { payload } = await jwtVerify(jwtString, JWKS, {
issuer: 'https://api.insumermodel.com',
algorithms: ['ES256'],
});
// payload.pass is the verified boolean
// payload.conditionHash, payload.blockNumber, payload.blockTimestamp
// are also verified as part of the signed JWT
return payload;
}
```
`createRemoteJWKSet` caches the JWKS automatically with sane defaults. Don't fetch the JWKS yourself on every call.
## Recipe 2: JWT verification (Python, `PyJWT` + `cryptography`)
```python
import jwt
from jwt.jwks_client import PyJWKClient
jwks_client = PyJWKClient("https://insumermodel.com/.well-known/jwks.json")
def verify_attestation(jwt_string: str) -> dict:
signing_key = jwks_client.get_signing_key_from_jwt(jwt_string)
payload = jwt.decode(
jwt_string,
signing_key.key,
algorithms=["ES256"],
issuer="https://api.insumermodel.com",
)
# payload["pass"] is the verified boolean
return payload
```
## Recipe 3: Raw `sig` verification (Node.js)
For any response's `sig` (every trust profile, and attestations without `format: "jwt"`). Pass `response.data` for `/v1/attest` and `/v1/trust`, or one entry of `data.results[]` for `/v1/trust/batch`, exactly as parsed from the wire; never rebuild or re-order the signed object, because the v1 scheme signs insertion-order `JSON.stringify` output.
```javascript
// Canonical JSON: keys sorted recursively at every level, no whitespace.
function canonicalize(value) {
if (Array.isArray(value)) return '[' + value.map(canonicalize).join(',') + ']';
if (value && typeof value === 'object') {
return '{' + Object.keys(value).sort()
.map((k) => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}';
}
return JSON.stringify(value);
}
// The exact bytes the ES256 signature covers, selected by kid.
function signedPreimage(data) {
const { kid } = data;
if (data.attestation) {
const { id, pass, results, attestedAt } = data.attestation;
if (kid === 'insumer-attest-v2') {
return 'insumer.attestation.v2\n' + canonicalize({ v: 2, id, pass, results, attestedAt });
}
if (kid === 'insumer-attest-v1') return JSON.stringify({ id, pass, results, attestedAt });
} else if (data.trust) {
if (kid === 'insumer-trust-v2') return 'insumer.trust.v2\n' + canonicalize(data.trust);
if (kid === 'insumer-attest-v1') return JSON.stringify(data.trust);
}
throw new Error(`kid ${kid} does not sign this artifact`); // fail closed
}
async function verifyRawSig(data, jwks) {
const jwk = jwks.keys.find((k) => k.kty === 'EC' && k.kid === data.kid);
if (!jwk) throw new Error(`unknown kid ${data.kid}`); // fail closed
const key = await crypto.subtle.importKey(
'jwk', { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y },
{ name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
// Web Crypto takes the 64-byte P1363 (r || s) signature as is.
return crypto.subtle.verify(
{ name: 'ECDSA', hash: 'SHA-256' }, key,
Buffer.from(data.sig, 'base64'),
new TextEncoder().encode(signedPreimage(data)));
}
```
This checks the ES256 signature only. It does not check condition hashes, expiry, or the post-quantum signature. For all of those, use the official package, **`insumer-verify`** on npm (ES module; import it, do not `require` it):
```bash
npm install insumer-verify @noble/post-quantum
```
`@noble/post-quantum` is an optional peer of `insumer-verify`. Without it, every response still verifies classically, but `checks.pq.status` reports `unverifiable` for a post-quantum signature that is present, so the post-quantum signature is never actually checked.
In Python, the same package is on PyPI under the same name, runs the same checks and passes the same 27 published test vectors:
```bash
pip install "insumer-verify[pq]"
```
```python
from insumer_verify import verify_attestation, verify_trust_profile
opts = {"jwks_url": "https://insumermodel.com/.well-known/jwks.json"}
result = verify_attestation(response, **opts) # the full /v1/attest response, or a JWT string
if result["valid"] and response["data"]["attestation"]["pass"]:
...
profile = verify_trust_profile(trust_response, **opts) # render profile["trust"] only when profile["valid"]
```
Without `dilithium-py` (the `[pq]` extra) the post-quantum signature is reported `unverifiable`, exactly as the npm package behaves without `@noble/post-quantum`.
```javascript
import { verifyAttestation, verifyTrustProfile } from 'insumer-verify';
const opts = { jwksUrl: 'https://insumermodel.com/.well-known/jwks.json' };
// /v1/attest: pass the full response ({ ok, data: { attestation, sig, kid, pqSig, pqKid, ... } }).
const result = await verifyAttestation(response, opts);
// result.valid is the AND of the checks; result.checks reports each one separately:
// checks.signature, checks.conditionHashes, checks.freshness, checks.expiry, and
// checks.pq (the post-quantum signature: status verified | refuted | absent | unverifiable).
// For a whole format:"jwt" response (data.jwt beside data.attestation) there is also
// checks.jwt, which verifies the tokens and binds them to the attestation.
if (!result.valid) throw new Error('attestation rejected: ' + JSON.stringify(result.checks));
// /v1/trust: pass the full response; for /v1/trust/batch, call once per data.results[i].
const trust = await verifyTrustProfile(trustResponse, opts);
// trust.checks: signature, freshness, expiry, pq. Render trust.trust (the verified object), gated on trust.valid.
if (!trust.valid) throw new Error('trust profile rejected: ' + JSON.stringify(trust.checks));
```
## Recipe 4: Conditional verification + tamper detection
Beyond signature verification, you can independently re-derive the `conditionHash` to confirm the condition wasn't tampered with:
```javascript
import { createHash } from 'node:crypto';
// conditionHash = "0x" + SHA-256 over evaluatedCondition, serialized per the kid:
// v2 kids: canonical JSON, keys sorted recursively at every level, no whitespace.
// insumer-attest-v1: JSON.stringify with the sorted top-level keys as the replacer.
function canonicalize(value) {
if (Array.isArray(value)) return '[' + value.map(canonicalize).join(',') + ']';
if (value && typeof value === 'object') {
return '{' + Object.keys(value).sort()
.map((k) => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}';
}
return JSON.stringify(value);
}
function recomputeConditionHash(evaluatedCondition, kid) {
const bytes = kid === 'insumer-attest-v1'
? JSON.stringify(evaluatedCondition, Object.keys(evaluatedCondition).sort())
: canonicalize(evaluatedCondition);
return '0x' + createHash('sha256').update(bytes).digest('hex');
}
// After signature verification (Recipe 1), re-derive and compare every condition.
// In the JWT payload, conditionHash is an array (one entry per condition) and each
// evaluatedCondition lives in results[i]. kid is the JWT header's kid
// (jwtVerify returns it as protectedHeader.kid).
payload.results.forEach((r, i) => {
const recomputed = recomputeConditionHash(r.evaluatedCondition, kid);
if (recomputed !== r.conditionHash || recomputed !== payload.conditionHash[i]) {
throw new Error(`conditionHash mismatch on condition ${i}: payload may have been tampered with`);
}
});
```
For the raw form, the same loop runs over `data.attestation.results` with `data.kid`.
## Code emission rules
1. **Cache the JWKS, not the verdict.** Libraries like `jose`'s `createRemoteJWKSet` and `PyJWT`'s `PyJWKClient` cache automatically with TTL. Do not cache `pass`: wallet state changes and the attestation has an `expiresAt` 30 minutes out (5 with a delegation condition).
2. **Pin the algorithm.** Always pass `algorithms: ['ES256']`. Never accept any algorithm. This blocks "alg confusion" attacks.
3. **Pin the issuer.** Always pass `issuer: 'https://api.insumermodel.com'` for JWT verification.
4. **Verify in the trust boundary.** Verify on the server that's making the access decision. Never verify in the browser and trust the result. (Browsers can verify; they just can't be the trust boundary.)
5. **Fail closed.** If verification throws, deny access. Never default to "allow" on verification failure.
## Helper script
`scripts/verify.py`: Python helper that takes a JWT, or a response object carrying `jwt` (from `/v1/attest` with `format: "jwt"`), on stdin and verifies the JWT against the public JWKS. Prints `OK` + payload, or `INVALID` + reason. Given a response with no `jwt`, or when the signing key cannot be fetched or resolved, it prints `NOT CHECKED` and exits non-zero: that is not a verdict on the token. It does not verify raw `sig` responses (every trust profile, and attestations without `format: "jwt"`), condition hashes, the expiry binding or the post-quantum signature. For every check in Python, `pip install "insumer-verify[pq]"` (above) is the complete path (without the `[pq]` extra the post-quantum signature is reported `unverifiable`); use Recipe 3 or `insumer-verify` on npm in JavaScript.
```bash
echo '{"jwt":"eyJhbG...","kid":"insumer-attest-v2"}' | python scripts/verify.py
```
## Error handling
| Symptom | Cause | Fix |
|---|---|---|
| "unknown kid" | A stale JWKS cache, or a result InsumerAPI did not sign | Refresh the JWKS. Kids are never removed, so one that still does not resolve is unverifiable: do not accept the result |
| "JWT signature invalid" | Payload tampered, or wrong public key | Confirm `kid` matches a JWKS entry, confirm algorithm pinned to ES256 |
| "JWT issuer mismatch" | Issuer claim doesn't match `https://api.insumermodel.com` | Confirm response actually came from InsumerAPI |
| "JWT expired" | Past its `exp` (30 min, or 5 with a delegation condition) | Re-request a fresh attestation; do not extend TTL |
| "conditionHash mismatch" | Condition object was modified after signing | Untrusted payload: reject |
## Related skills
| Skill | Purpose |
|---|---|
| `insumer-auth` | Get a key (verifying responses doesn't need a key, but requesting them does) |
| `insumer-attest` | Produces signed responses to verify |
| `insumer-trust` | Produces signed responses to verify |
| `insumer-trust-batch` | Verify each profile entry independently |
## References
- [jwks-format.md](references/jwks-format.md): full JWKS document shape, key rotation policy, JWT claim list
- [`insumer-verify` on npm](https://www.npmjs.com/package/insumer-verify): canonical raw-sig verification package
- [Public JWKS](https://insumermodel.com/.well-known/jwks.json): fetch the live key set
- [JWT spec (RFC 7519)](https://datatracker.ietf.org/doc/html/rfc7519)
- [JWS spec (RFC 7515)](https://datatracker.ietf.org/doc/html/rfc7515)
Referenced files: 2 — file references are metadata, not package contents.
insumer-trust13.3 KB
View saved version →
---
name: insumer-trust
description: >
InsumerAPI wallet trust profile: a curated multi-dimensional condition-based
access bundle for a single wallet. 155 base checks across 27 chains in 10
dimensions (stablecoins, governance, NFTs, staking, institutional
stablecoins, tokenized treasuries, stablecoin deposits, wrapped bitcoin,
names, account), plus optional Solana, XRPL, Bitcoin and Tron dimensions (up
to 176 checks across 29 chains in 14 dimensions). Use when the user wants an InsumerAPI
trust profile, a pre-built signed wallet snapshot rather than conditions
specified one by one (for example an InsumerAPI pre-transaction trust check). The profile is signed once, as a whole. Carry it unchanged:
never re-sign or wrap it.
metadata:
version: "0.3.1"
author: InsumerAPI
---
# InsumerAPI Wallet Trust Profile
A curated condition bundle for a single wallet. Same primitive as `insumer-attest` (read → evaluate → sign), but the conditions are pre-defined: 155 base checks across 27 chains in 10 dimensions, with optional cross-chain extensions. **Boolean, not balance, on every check.** Every row is a presence check.
Pick this skill when the developer wants a snapshot. Pick `insumer-attest` when they want to specify their own conditions.
## What you get
- **155 base checks** across **27 chains** in **10 dimensions** (23 EVM chains plus the Solana, XRPL, Stellar and Sui rows inside the base dimensions):
- **Stablecoins** (52): USDC, USDT, OUSD, PYUSD, USDG, USD1, RLUSD, USDS, DAI and EURC across 23 EVM chains, including USDC on Arc and USDG on Robinhood Chain
- **Governance** (8): UNI, AAVE, ENS, LDO, SKY and COMP on Ethereum, ARB on Arbitrum, OP on Optimism
- **NFTs** (3): BAYC, Pudgy Penguins, Wrapped CryptoPunks
- **Staking** (5): stETH, rETH, cbETH, wstETH, weETH
- **Institutional stablecoins** (8): EURCV, USDCV, USDC, and BENJI across Ethereum, Solana, XRPL, Stellar, and Sui. Always present; the Solana, XRPL, Stellar and Sui entries are evaluated only when the matching wallet is supplied, and are otherwise marked `evaluated: false`
- **Tokenized treasuries** (16): BUIDL, USYC, OUSG, USTB, USDY (the USDY on Sui row needs `suiWallet`, otherwise `evaluated: false`)
- **Stablecoin deposits** (39): Aave v3 aUSDC/aUSDT, sUSDS, sDAI, listed Morpho USDC vaults
- **Wrapped bitcoin** (12): cbBTC, WBTC, tBTC
- **Names** (2): ENS .eth, Basenames
- **Account** (10): two rows per chain on Ethereum, Base, Arbitrum, Optimism and Polygon. "Contract code on X" is met when bytecode other than the EIP-7702 delegation designator is at the wallet address; "EIP-7702 delegation on X" is met when the designator is there. The two are exclusive per chain; a plain key account reads false on both. Which contract is never named; to require a specific delegation target, send `delegate` with an `account_code` condition in `insumer-attest` (the answer is still yes or no). Each row's `evaluatedCondition` is `{type: "account_code", chainId, expect, operator: "code_state"}`, hashed exactly like the equivalent attest condition
- **Optional extensions** (when extra wallet addresses are provided):
- **Solana** (14): pass `solanaWallet` (USDC, EURC, OUSD, PYUSD, USD1, USDG, USDS, BUIDL, USDY, WBTC, cbBTC, tBTC, JitoSOL, mSOL on Solana)
- **XRPL** (3): pass `xrplWallet` (RLUSD, USDC, OUSG)
- **Bitcoin** (1): pass `bitcoinWallet` (native BTC)
- **Tron** (3): pass `tronWallet` (USDT, USD1, WBTC)
- **Stellar / Sui**: pass `stellarWallet` / `suiWallet`. These add no checks and no dimension; they let the Stellar and Sui rows inside the base dimensions be evaluated
- Up to 176 total checks across 29 chains in 14 dimensions when all extensions are included
- Each check returns its own boolean; the response includes per-dimension and overall summaries
- Dimension order is fixed: the base dimensions in the order above (`stablecoins`, `governance`, `nfts`, `staking`, `institutional_stablecoins`, `tokenized_treasuries`, `stablecoin_deposits`, `wrapped_bitcoin`, `names`, `account`), then whichever of `solana`, `xrpl`, `bitcoin`, `tron` were switched on, in that order
- `conditionSetVersion` is a dated set id (currently `"2026-10-08"`), the same on every key version, signed with the profile. It names the check list that was run: log it, never reject on it
- 3 credits standard, 6 credits with `proof: "merkle"`
## Architectural property to preserve
The whole trust profile is signed once by InsumerAPI: one `sig` covers the entire `trust` object. The response is signed twice: ES256 (`sig`, `kid`) and a post-quantum ML-DSA-65 signature (`pqSig`, `pqKid`, optional fields: check them when present). The `kid` is `insumer-trust-v2` on v2 keys and `insumer-attest-v1` on v1 keys: read it from the response, never assume it. There are no per-dimension or per-check signatures. **Do not add an orchestrator-layer wrap.** If you build a multi-issuer trust envelope that includes this profile, carry the signed object exactly as issued, beside the other issuers' signed objects, and never re-sign it or wrap it under a new envelope signature. Verifiers should be able to check InsumerAPI's signature directly against its JWKS.
## Setup
```bash
export INSUMER_API_KEY='insr_live_...'
```
## Reference values
- **Endpoint**: `POST https://api.insumermodel.com/v1/trust`
- **Cost**: 3 credits standard, 6 credits with `proof: "merkle"`
- **Wallet pattern**: `wallet` is required (EVM, `0x` + 40 hex chars). `solanaWallet`, `xrplWallet`, `bitcoinWallet`, `tronWallet`, `stellarWallet` (G-address) and `suiWallet` (`0x` + 64 hex chars) are optional add-ons.
- **Trust profile ID format**: `TRST-XXXXX` (returned in `data.trust.id`)
## Usage
### Example 1: EVM-only profile
```bash
curl -X POST https://api.insumermodel.com/v1/trust \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0x1601843c5E9bC251A3272907010AFa41Fa18347E"
}'
```
Response shape (abbreviated; the counts are from a real profile of this wallet, a contract on all five account chains):
```json
{
"ok": true,
"data": {
"trust": {
"id": "TRST-81224",
"wallet": "0x1601843c5E9bC251A3272907010AFa41Fa18347E",
"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": 2, "notEvaluatedCount": 6, "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 }
},
"summary": { "totalChecks": 155, "totalPassed": 16, "totalFailed": 132, "totalNotEvaluated": 7, "dimensionsWithActivity": 3, "dimensionsChecked": 10 },
"profiledAt": "2026-10-07T21:53:04.795Z",
"expiresAt": "2026-10-07T22:23:04.795Z"
},
"sig": "...",
"kid": "insumer-trust-v2",
"pqSig": "...",
"pqKid": "insumer-trust-pq1"
},
"meta": {
"creditsRemaining": ...,
"creditsCharged": 3
}
}
```
The first two `account` rows of that profile, as signed:
```json
{"label":"Contract code on Ethereum","chainId":1,"met":true,"evaluatedCondition":{"type":"account_code","chainId":1,"expect":"contract","operator":"code_state"},"conditionHash":"0xfd7b6aa42eb012184fa54d9d5d99c6ab18481e0ce33ec0ec0e9d28d37b54ccbc","blockNumber":"0x18eea5a","blockTimestamp":"2026-10-07T21:52:59.000Z"}
{"label":"EIP-7702 delegation on Ethereum","chainId":1,"met":false,"evaluatedCondition":{"type":"account_code","chainId":1,"expect":"eip7702","operator":"code_state"},"conditionHash":"0xdef6fadcef95f59f4621fa2bf788e6be0ffc0492dba22038999b8cd757adf18b","blockNumber":"0x18eea5a","blockTimestamp":"2026-10-07T21:52:59.000Z"}
```
### Example 2: With a Solana wallet
```bash
curl -X POST https://api.insumermodel.com/v1/trust \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"solanaWallet": "DXK4yMpigbTSqv33nJk1tJucXB4E3rDmTXn3yZmiFAXt"
}'
```
### Example 3: Multi-chain profile (EVM + Solana + XRPL + Bitcoin)
```bash
curl -X POST https://api.insumermodel.com/v1/trust \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"solanaWallet": "DXK4yMpigbTSqv33nJk1tJucXB4E3rDmTXn3yZmiFAXt",
"xrplWallet": "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH",
"bitcoinWallet": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
}'
```
Each provided Solana, XRPL, Bitcoin or Tron wallet adds its dimension to the response. A Stellar or Sui wallet (`stellarWallet`, `suiWallet`) adds no dimension; it lets the matching rows inside the base dimensions be evaluated instead of marked `evaluated: false`: the Stellar and Sui institutional-stablecoin entries, and for Sui also USDY on Sui in tokenized treasuries.
### Example 4: Merkle proofs (advanced)
```bash
curl -X POST https://api.insumermodel.com/v1/trust \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"proof": "merkle"
}'
```
An EVM token row carries an EIP-1186 storage proof when its balance slot can be discovered. Computed-balance rows (Aave aTokens, BUIDL), rows with non-standard storage layouts, and rows on chains that serve no proofs are declined with a reason; NFT and non-EVM rows are declined; `account` rows carry `proof.available: false` with a reason pointing at `/v1/attest`, where an `account_code` condition in proof mode carries the account proof; unevaluated checks carry no proof key. **Costs 6 credits** instead of 3; the premium is refunded whenever no proof could be delivered. Proofs reveal raw on-chain balances. Only opt in if the consumer explicitly needs the raw balances.
## When to use trust profile vs. custom attest
| User asks for... | Use |
|---|---|
| "Verify this wallet holds X on chain Y" | `insumer-attest` |
| "Gate by USDC balance on Base" | `insumer-attest` |
| "Pre-transaction trust check" | `insumer-trust` |
| "Show me what this wallet holds across chains" | `insumer-trust` |
| "Multi-dimensional wallet profile" | `insumer-trust` |
| "Should I transact with this wallet" | `insumer-trust` |
For multiple wallets in one call, use `insumer-trust-batch`.
## Code emission rules
1. **Read the API key from an env var.** Never inline.
2. **Verify the signature offline.** Use `insumer-jwks-verify`. The signed boolean(s) is the product.
3. **Don't re-sign or wrap the profile.** One InsumerAPI signature covers the whole `trust` object; placing it under a new envelope signature defeats the verification chain. A check whose wallet was not supplied is marked `evaluated: false` and counted in `totalNotEvaluated`, never as a failure.
4. **Don't cache the verdict.** Cache the JWKS, not the trust profile result. Wallet state changes.
5. **Backend only.** The API key is a backend credential.
## Helper script
`scripts/trust.py`: Python helper that wraps `POST /v1/trust`. Reads `INSUMER_API_KEY` from env, accepts `--wallet`, `--solana`, `--xrpl`, `--bitcoin`, `--tron`, `--stellar`, `--sui`, `--proof merkle`.
```bash
python scripts/trust.py --wallet 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
```
## Error handling
| Status | Cause | Fix |
|---|---|---|
| `400` | Missing `wallet` or invalid format | EVM wallet `^0x[a-fA-F0-9]{40}$` is required |
| `401` | Invalid API key | See `insumer-auth` |
| `402` | With a key: out of credits. With no key at all: a pay-per-call quote, not a credit problem | With a key, top up via Path 4 in `insumer-auth`. With no key, set `INSUMER_API_KEY` and call again |
| `429` | Daily request limit reached | It resets on a timer, not by slowing down: wait the number of seconds in the `Retry-After` header |
| `503` | Upstream blockchain data source unavailable | Retryable; no credits charged |
A `503` carries error code `rpc_failure`. It means a read did not complete and nothing was signed. It is never a `false`: do not report it as a check that was not held. Retry the call.
## Related skills
| Skill | Purpose |
|---|---|
| `insumer-auth` | API key creation, top-up |
| `insumer-attest` | Custom condition (pick this if not using the curated bundle) |
| `insumer-trust-batch` | Same primitive for multiple wallets in one call |
| `insumer-jwks-verify` | Offline ES256 verification |
## References
- [InsumerAPI OpenAPI spec](https://insumermodel.com/openapi.yaml)
- [Public JWKS](https://insumermodel.com/.well-known/jwks.json)
- [Developer docs: trust profile](https://insumermodel.com/developers/trust/)
Referenced files: 1 — file references are metadata, not package contents.
insumer-trust-batch11.2 KB
View saved version →
---
name: insumer-trust-batch
description: >
InsumerAPI batch wallet trust profiles: the same curated bundle as
insumer-trust, for up to 10 wallets in a single call. Use when the user wants
InsumerAPI trust profiles for a list of wallets (for example screening an
airdrop or allowlist with InsumerAPI). Each wallet's profile is independently
signed; the response supports partial success.
metadata:
version: "0.4.3"
author: InsumerAPI
---
# InsumerAPI Batch Wallet Trust Profile
Same curated condition-based access bundle as `insumer-trust` (155 base checks across 27 chains in 10 dimensions, up to 176 checks across 29 chains in 14 dimensions with the optional wallets; `conditionSetVersion` `"2026-10-08"`), but accepts up to **10 wallets** in one request. Each wallet's profile is independently signed; the response supports partial success: a failure for one wallet does not fail the rest.
Each wallet's profile is signed once, as a whole, by InsumerAPI, and signed twice: ES256 (`sig`, `kid`) and a post-quantum ML-DSA-65 signature (`pqSig`, `pqKid`). The `kid` is `insumer-trust-v2` on v2 keys and `insumer-attest-v1` on v1 keys: read it from each entry, never assume it. There are no per-dimension signatures and no signature over the batch. **Do not add an orchestrator wrap:** carry each signed profile exactly as issued.
## Setup
```bash
export INSUMER_API_KEY='insr_live_...'
```
## Reference values
- **Endpoint**: `POST https://api.insumermodel.com/v1/trust/batch`
- **Cost**: 3 credits per **successful** wallet (6 with `proof: "merkle"`). Failed wallets in the batch don't cost credits.
- **Max wallets per call**: 10
- **Profile ID format per wallet**: `TRST-XXXXX`
## Request shape
`wallets` is an **array of objects**, each with a required `wallet` field plus optional cross-chain wallet fields. (Not an array of plain strings.)
```json
{
"wallets": [
{
"wallet": "0x1601843c5E9bC251A3272907010AFa41Fa18347E",
"solanaWallet": "DXK4yMpigbTSqv33nJk1tJucXB4E3rDmTXn3yZmiFAXt"
},
{ "wallet": "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb" }
]
}
```
| Field per entry | Required | Notes |
|---|---|---|
| `wallet` | yes | EVM address, `0x` + 40 hex chars |
| `solanaWallet` | optional | Adds the 14-check `solana` dimension to this wallet's profile and evaluates the Solana rows in institutional stablecoins |
| `xrplWallet` | optional | Adds the `xrpl` dimension (RLUSD, USDC, OUSG) and evaluates the XRPL row in institutional stablecoins |
| `bitcoinWallet` | optional | Adds the `bitcoin` dimension (native BTC) |
| `tronWallet` | optional | Adds the `tron` dimension (USDT, USD1, WBTC) |
| `stellarWallet` | optional | Evaluates the Stellar rows in the institutional stablecoins dimension; adds no dimension |
| `suiWallet` | optional | Evaluates the Sui rows (USDC in institutional stablecoins, USDY in tokenized treasuries); adds no dimension |
Top-level `proof: "merkle"` (optional) applies to all wallets in the batch and costs up to 6 credits per successful wallet (3 for a wallet whose profile carried no proof; a failed wallet costs nothing).
## Response shape
Abbreviated; the profile ids and counts are from real profiles of the two wallets above, and the batch totals are those of a two-wallet call (the first with its Solana wallet, so its profile has 11 dimensions and 169 checks; the second has the 10 base dimensions and 155 checks):
```json
{
"ok": true,
"data": {
"results": [
{
"trust": {
"id": "TRST-74167",
"wallet": "0x1601843c5E9bC251A3272907010AFa41Fa18347E",
"conditionSetVersion": "2026-10-08",
"dimensions": { ... },
"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"
},
"sig": "...",
"kid": "insumer-trust-v2",
"pqSig": "...",
"pqKid": "insumer-trust-pq1"
},
{
"trust": {
"id": "TRST-C7EA2",
"wallet": "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
"conditionSetVersion": "2026-10-08",
"dimensions": { ... },
"summary": { "totalChecks": 155, "totalPassed": 30, "totalFailed": 118, "totalNotEvaluated": 7, "dimensionsWithActivity": 7, "dimensionsChecked": 10 },
"profiledAt": "2026-10-07T21:53:03.495Z",
"expiresAt": "2026-10-07T22:23:03.495Z"
},
"sig": "...",
"kid": "insumer-trust-v2",
"pqSig": "...",
"pqKid": "insumer-trust-pq1"
}
],
"summary": { "requested": 2, "succeeded": 2, "failed": 0 }
},
"meta": {
"creditsCharged": 6,
"creditsRemaining": ...
}
}
```
Each entry in `data.results[]` is **either** a `{trust, sig, kid, pqSig, pqKid}` object (`pqSig` and `pqKid` are optional fields: check them when present) **or** `{error: { wallet, message }}`. Iterate, branch, and verify each `trust` independently with `insumer-jwks-verify`. Every profile lists its dimensions in the same fixed order: `stablecoins`, `governance`, `nfts`, `staking`, `institutional_stablecoins`, `tokenized_treasuries`, `stablecoin_deposits`, `wrapped_bitcoin`, `names`, `account`, then whichever of `solana`, `xrpl`, `bitcoin`, `tron` that wallet switched on, in that order. The `account` dimension (10 rows: contract code and EIP-7702 delegation on Ethereum, Base, Arbitrum, Optimism and Polygon) is a presence check on the wallet's own code state; in proof mode its rows carry `proof.available: false` with a reason pointing at `/v1/attest`.
## Usage
### Example 1: Two EVM wallets
```bash
curl -X POST https://api.insumermodel.com/v1/trust/batch \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallets": [
{ "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
{ "wallet": "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B" }
]
}'
```
### Example 2: Mixed cross-chain coverage per wallet
```bash
curl -X POST https://api.insumermodel.com/v1/trust/batch \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallets": [
{
"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"solanaWallet": "5v9CXTpN3WHbM2jAYty88qDGz7P4yuMv8fnuPRXBPmiB"
},
{
"wallet": "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
"xrplWallet": "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH",
"bitcoinWallet": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
}
]
}'
```
### Example 3: Batch with Merkle proofs
```bash
curl -X POST https://api.insumermodel.com/v1/trust/batch \
-H "X-API-Key: $INSUMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wallets": [
{ "wallet": "0x..." },
{ "wallet": "0x..." }
],
"proof": "merkle"
}'
```
Cost: up to `successful_wallets * 6` credits; a wallet whose profile carried no proof costs 3. Reveals raw on-chain balances. Only opt in if needed.
## When to use batch vs single
| Situation | Use |
|---|---|
| Single wallet | `insumer-trust` |
| 2–10 wallets, same call | `insumer-trust-batch` |
| More than 10 wallets | Loop over batches of 10 |
| Different conditions per wallet | Loop over `insumer-attest` (custom conditions) |
| Same custom conditions for many wallets | Loop over `insumer-attest` |
## Code emission rules
1. **Read the API key from an env var.** Never inline.
2. **Iterate `data.results[]` and branch on `trust` vs `error`.** Don't assume every entry succeeded.
3. **Verify each profile's signature independently.** The batch wrapper does not produce an aggregate signature; each entry in `data.results[]` carries its own `sig` and `kid`.
4. **Cap at 10 wallets per call.** For larger lists, batch client-side.
5. **Pre-validate wallet format** before sending. Invalid `wallet` formats fail validation for the entire request.
6. **Backend only.** The API key is a backend credential.
7. **Don't cache the verdict.** Wallet state changes.
## Helper script
`scripts/trust_batch.py`: Python helper. Reads `INSUMER_API_KEY` from env, accepts a wallet list via `--wallets-file` (one EVM address or one JSON object per line) or `--wallets w1,w2,w3`. Builds the proper array-of-objects request body.
```bash
python scripts/trust_batch.py --wallets-file allowlist.txt
```
The script prints a summary per wallet (profile ID, held / not held / not evaluated counts, and the checks held in each dimension) and saves the complete signed response to a JSON file named in the summary; `--out PATH` chooses the file. The dimensions print in a fixed order, the same for every wallet: the ten base dimensions (`stablecoins` through `names`, then `account`), then `solana`, `xrpl`, `bitcoin`, `tron` where present. The `account` dimension reads "present" rather than "held" (its rows are code states at the wallet address, not holdings): for example `account: 5 of 10 present: Contract code on Ethereum, Contract code on Base, ...`. Read the summary to answer the user, and verify each entry of `data.results` in the saved file on the raw `sig` path (for example `verify_trust_profile` from the `insumer-verify` package, or the raw-sig instructions in `insumer-jwks-verify`). The script never overwrites a file; if it cannot save one, it prints the complete response instead, so a paid call is never lost. A full profile is tens of thousands of characters, so ten printed in full are more than an agent can read at once; `--full` prints the complete response as well as saving it. Profiles cannot be fetched again, so keep the saved file rather than calling a second time.
For per-wallet cross-chain coverage, put one JSON object per line in the `--wallets-file` (for example `{"wallet": "0x...", "solanaWallet": "..."}`), or call the API directly with curl.
## Error handling
| Status | Cause | Fix |
|---|---|---|
| `400` | Missing `wallets`, exceeds 10, or invalid wallet format | Validate addresses; split into ≤10 batches |
| `401` | Invalid API key | See `insumer-auth` |
| `402` | With a key: insufficient credits for the entire batch. With no key at all: a pay-per-call quote, not a credit problem | With a key, top up via Path 4 in `insumer-auth`. With no key, set `INSUMER_API_KEY` and call again |
| `429` | Daily request limit reached | It resets on a timer, not by slowing down: wait the number of seconds in the `Retry-After` header |
The batch endpoint does not answer `503` for an upstream data-source failure. A wallet whose chain reads fail is refused on its own: it appears as an `{error: {wallet, message}}` entry in `data.results[]` of a `200` response, is never signed, and does not consume credits. Such an entry is never a `false`: it says the wallet was not read, not that it holds nothing. Retry those wallets later.
## Related skills
| Skill | Purpose |
|---|---|
| `insumer-auth` | API key creation, top-up |
| `insumer-trust` | Single-wallet curated profile |
| `insumer-attest` | Custom condition (single call, up to 10 conditions) |
| `insumer-jwks-verify` | Offline signature verification: apply per `data.results[]` entry |
## References
- [InsumerAPI OpenAPI spec](https://insumermodel.com/openapi.yaml)
- [Public JWKS](https://insumermodel.com/.well-known/jwks.json)
- [Developer docs: trust profile](https://insumermodel.com/developers/trust/)
Referenced files: 1 — file references are metadata, not package contents.