insumer-verify
v1.5.1
Published
Client-side verifier for InsumerAPI condition-based access attestations. ECDSA P-256 signatures, condition hashes, block freshness, expiry. Zero dependencies.
Maintainers
Readme
insumer-verify
Client-side verifier for InsumerAPI wallet auth attestations. Validates ECDSA P-256 signatures, condition hashes, block freshness, and attestation expiry. Zero runtime dependencies. Web Crypto API. Node.js 18+ and modern browsers. Type-agnostic: verifies every condition type (token_balance, nft_ownership, eas_attestation, farcaster_id, ratio_to_amount, ratio_to_supply) by recomputing the condition hash from the signed evaluatedCondition.
Part of the InsumerAPI ecosystem: REST API (26 endpoints, 37 chains) | MCP server (npm) | LangChain (PyPI) | ElizaOS (10 actions, npm) | OpenAI GPT (GPT Store)
Install
npm install insumer-verifyGet an API Key
Generate one from your terminal — no browser needed:
curl -s -X POST https://api.insumermodel.com/v1/keys/create \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "appName": "insumer-verify", "tier": "free"}' | jq .Returns an insr_live_... key with 100 reads/day and 10 verification credits. One free key per email.
Or get one at insumermodel.com/developers.
Usage
Call InsumerAPI, get a signed attestation, verify it:
import { verifyAttestation } from "insumer-verify";
// 1. Call InsumerAPI
const res = await fetch("https://api.insumermodel.com/v1/attest", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "insr_live_your_key_here",
},
body: JSON.stringify({
wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
conditions: [
{
type: "token_balance",
chainId: 1,
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
threshold: "1000", // decimal string — new keys are v2 and reject a JSON number with 400
decimals: 6,
label: "USDC >= 1000 on Ethereum",
},
],
}),
});
const apiResponse = await res.json();
// 2. Verify the attestation (signature, condition hashes, freshness, expiry)
const result = await verifyAttestation(apiResponse, {
jwksUrl: "https://insumermodel.com/.well-known/jwks.json",
maxAge: 120,
});
if (result.valid) {
const { pass, results } = apiResponse.data.attestation;
console.log(`All conditions ${pass ? "met" : "not met"}`);
for (const r of results) {
console.log(` ${r.label}: ${r.met ? "met" : "not met"}`);
}
} else {
console.log("Verification failed:", result.checks);
}What the API returns
The attestation response you're verifying looks like this:
{
"ok": true,
"data": {
"attestation": {
"id": "ATST-A7C3E1B2D4F56789",
"pass": true,
"results": [
{
"condition": 0,
"met": true,
"label": "USDC >= 1000 on Ethereum",
"type": "token_balance",
"chainId": 1,
"evaluatedCondition": {
"chainId": 1,
"contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"decimals": 6,
"operator": "gte",
"threshold": 1000,
"type": "token_balance"
},
"conditionHash": "0x554251734232c8b43062f1cf2bb51b76650d13268104d74c645f4893e67ef69c",
"blockNumber": "0x129e3f7",
"blockTimestamp": "2026-02-28T12:34:56.000Z"
}
],
"passCount": 1,
"failCount": 0,
"attestedAt": "2026-02-28T12:34:57.000Z",
"expiresAt": "2026-02-28T13:04:57.000Z"
},
"sig": "dmNJKqnGZ9f47qpWax9gxgw1DhUKHKHrbLspTop8NWzYhv2fNpVAt1gAuhUfU4xPsgXTCdrmTXI4vEE50dcfEA==",
"kid": "insumer-attest-v1"
},
"meta": { "version": "1.0", "timestamp": "2026-03-26T20:04:34.153Z", "creditsRemaining": 99, "creditsCharged": 1 }
}No balances. No amounts. Just a signed true/false per condition.
XRPL-specific fields
XRPL attestation results use ledgerIndex (integer) and ledgerHash (string) instead of blockNumber and blockTimestamp:
{
"condition": 0,
"met": true,
"label": "RLUSD >= 100 on XRPL",
"type": "token_balance",
"chainId": "xrpl",
"evaluatedCondition": {
"type": "token_balance",
"chainId": "xrpl",
"contractAddress": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De",
"currency": "524C555344000000000000000000000000000000",
"operator": "gte",
"threshold": 100,
"decimals": 6
},
"conditionHash": "0x9f2c...",
"ledgerIndex": 96482715,
"ledgerHash": "A1B2C3D4..."
}Trust line token conditions may also include trustLineState with a frozen boolean indicating whether the trust line is frozen:
"trustLineState": { "frozen": false }Because XRPL results have no blockTimestamp, the freshness check (maxAge) skips them rather than failing. Condition hash and signature checks work identically across all chains.
Handling rpc_failure errors
If the API cannot reach one or more upstream data sources after retries, it returns ok: false with error code rpc_failure instead of issuing an attestation. No signature, no JWT, no credits charged. This is a retryable error — retry the same request after a short delay (2-5 seconds).
{
"ok": false,
"error": {
"code": "rpc_failure",
"message": "Unable to verify all conditions — data source unavailable after retries",
"failedConditions": [
{ "source": "rpc", "chainId": "43114", "message": "Timeout" }
]
},
"meta": { "version": "1.0", "timestamp": "..." }
}Important: rpc_failure is NOT a verification failure. It means the API could not check the condition at all. Do not treat it as pass: false. Check for ok: false with error.code === "rpc_failure" and retry:
const res = await fetch("https://api.insumermodel.com/v1/attest", { ... });
const data = await res.json();
if (!data.ok && data.error?.code === "rpc_failure") {
// Retryable — data source temporarily unavailable
// Wait 2-5 seconds and retry the same request
console.log("RPC failure, retrying...", data.error.failedConditions);
return;
}
// Only verify if we got an actual attestation
const result = await verifyAttestation(data, { maxAge: 120 });Browser
<script type="module">
import { verifyAttestation } from "https://esm.sh/insumer-verify";
// apiResponse = attestation from your backend
const result = await verifyAttestation(apiResponse, {
jwksUrl: "https://insumermodel.com/.well-known/jwks.json",
});
console.log(result.valid, result.checks);
</script>Freshness check
Pass maxAge (seconds) to reject attestations where block data is too old:
const result = await verifyAttestation(apiResponse, { maxAge: 120 });
// Fails if any blockTimestamp is older than 120 secondsResults without blockTimestamp (certain EVM chains, Solana, and XRPL) are skipped, not treated as failures. XRPL results use ledgerIndex and ledgerHash instead.
JWKS key discovery
By default, insumer-verify uses the hardcoded InsumerAPI public key. You can opt in to dynamic key discovery via the JWKS endpoint:
const result = await verifyAttestation(apiResponse, {
jwksUrl: "https://insumermodel.com/.well-known/jwks.json",
});When jwksUrl is set, the library fetches the JWKS, matches the key by kid from the attestation response, and uses it for signature verification. This enables automatic key rotation without library updates.
Trust contract. jwksUrl should be a hardcoded constant (e.g. the InsumerAPI JWKS endpoint) or another URL you control — set once at integration time. The library fetches whatever URL you pass, so passing untrusted user input would let a caller direct the library to fetch arbitrary endpoints on the host's behalf.
Signing scheme versions (v1 / v2)
InsumerAPI signs attestations with one of two schemes; the kid on each response selects which, and this library verifies both automatically — you don't need to do anything:
insumer-attest-v1— signature over the bareJSON.stringify({id, pass, results, attestedAt}). Frozen.insumer-attest-v2— signature over a domain-separated, canonical preimage:"insumer.attestation.v2\n"+ recursive-sorted-key canonical JSON of{v:2, id, pass, results, attestedAt}. Condition hashes use the same recursive canonicalization. In a v2evaluatedCondition,thresholdis a canonical decimal string and there is nodecimalsfield.
All newly issued API keys are v2. The JWKS publishes three kids (all the same key): insumer-attest-v1, insumer-attest-v2, and insumer-trust-v2 — resolve the key by kid. (This library verifies attestations; trust-profile (insumer-trust-v2) verification is not yet included.)
When you create a v2 key, send
/v1/attestthresholdvalues as decimal strings ("100"), not numbers — a JSON number is rejected with 400. This is a request-side requirement; it does not affect verification.
API
verifyAttestation(response, options?)
| Parameter | Type | Description |
|-----------|------|-------------|
| response | unknown | Full InsumerAPI response envelope (must contain data.attestation and data.sig) |
Common mistake: Pass the full API response (
await res.json()), notresponse.dataorresponse.data.attestation. The function expects the outer envelope{ok, data: {attestation, sig, kid}, meta}. Passing the innerdataobject will throw"Invalid response: missing data object". |options.maxAge|number| Optional max age in seconds for block freshness check | |options.jwksUrl|string| Optional JWKS URL for dynamic key discovery (e.g.https://insumermodel.com/.well-known/jwks.json) |
Returns Promise<VerifyResult>:
interface VerifyResult {
valid: boolean; // true only if ALL checks pass
checks: {
signature: { passed: boolean; reason?: string };
conditionHashes: { passed: boolean; failures?: number[]; reason?: string };
freshness: { passed: boolean; reason?: string };
expiry: { passed: boolean; reason?: string };
};
}What gets verified
| Check | What it does |
|-------|-------------|
| Signature | Verifies the ECDSA P-256 signature over {id, pass, results, attestedAt} using InsumerAPI's public key |
| Condition hashes | Recomputes SHA-256 of each evaluatedCondition (canonical sorted-key JSON) and compares to conditionHash |
| Freshness | Checks blockTimestamp age against caller-defined maxAge (optional, skipped if not set) |
| Expiry | Checks whether the attestation's 30-minute validity window has elapsed |
Condition hash specification
Each attestation result includes an evaluatedCondition object and a conditionHash. The hash is computed as:
- Extract all keys from
evaluatedConditionand sort them alphabetically - Serialize with
JSON.stringify(evaluatedCondition, sortedKeys)(the sorted keys array as the replacer produces canonical key order) - Compute SHA-256 of the UTF-8 encoded string
- Format as
"0x" + hex
This ensures the condition that was actually evaluated on-chain can be independently verified by any third party.
Examples
The examples/ directory contains runnable scripts covering common patterns:
| Example | What it shows |
|---------|--------------|
| basic-attest.mjs | Single token balance check + verification |
| multi-condition.mjs | Multiple conditions across different chains |
| jwt-format.mjs | JWT format for gateway integration (Kong, Nginx, Cloudflare Access) |
| verify-manual.mjs | DIY verification with Web Crypto — no library, proving the format is open |
| express-gate.mjs | Express middleware: verify attestation before granting access |
| xrpl-trustline.mjs | XRPL trust line tokens (RLUSD, USDC) |
INSUMER_API_KEY=insr_live_... node examples/basic-attest.mjsThe attestation format is an open standard — verify-manual.mjs demonstrates full verification using only the Web Crypto API with no dependencies. See the State Attestation Spec for the complete format definition.
Pricing
Tiers: Free (100 reads/day, 10 credits) | Pro $29/mo (1,000 credits/mo, 10,000/day) | Enterprise $99/mo (5,000 credits/mo, 100,000/day)
Volume discounts: $5–$99 = $0.04/call (25 credits/$1) · $100–$499 = $0.03 (33/$1, 25% off) · $500+ = $0.02 (50/$1, 50% off)
Platform wallets:
- EVM (USDC/USDT):
0xAd982CB19aCCa2923Df8F687C0614a7700255a23 - Solana (USDC/USDT):
6a1mLjefhvSJX1sEX8PTnionbE9DqoYjU6F6bNkT4Ydr - Bitcoin:
bc1qg7qnerdhlmdn899zemtez5tcx2a2snc0dt9dt0
Supported payment chains: Ethereum, Base, Polygon, Arbitrum, Optimism, BNB Chain, Avalanche, Solana, Bitcoin. Tokens sent on unsupported chains cannot be recovered. All purchases are final and non-refundable. Full pricing →
License
MIT
