Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions demos/insumer-wallet-state/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Insumer wallet-state attestations in ACK

Consume an **InsumerAPI** wallet-state attestation as a verifiable claim — so an
ACK service can gate access on *live on-chain wallet state*, not just identity or
proof-of-payment. Mirrors [`demos/skyfire-kya`](../skyfire-kya).

## Why

ACK-ID attests *who* an agent is, and ACK-Pay confirms a payment cleared — but
nothing checks the **paying wallet's on-chain state** before settlement: no
balance, holdings, or compliance condition on the payer.

InsumerAPI fills that gap. `POST /v1/attest` reads on-chain wallet state,
evaluates it against your conditions (token balance ≥ X, NFT ownership, EAS
attestation) across 37 chains, and returns an **ES256-signed boolean** — a
portable attestation anyone can verify offline with a public key.

## The key property

- **Verifying needs no secret.** Verification runs through the canonical
[`insumer-verify`](https://www.npmjs.com/package/insumer-verify) SDK — it checks
the ECDSA P-256 signature, the condition-hash binding, and expiry (and block
freshness when you pass `maxAge`) against the public JWKS at
`https://insumermodel.com/.well-known/jwks.json`. Verify offline; no key, nothing shared.
- **Only *minting* needs a key.** Requesting a fresh attestation calls
`POST /v1/attest` with your own `X-API-Key`.

## Run

```bash
# verify the bundled sample (no key)
pnpm demo

# mint fresh + verify + gate green
INSUMER_API_KEY=insr_live_... pnpm demo
```

Get a free key (no signup, one call):

```bash
curl -X POST https://api.insumermodel.com/v1/keys/create \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","appName":"ack-demo","tier":"free"}'
```

Attestations are valid 30 minutes by design; once the bundled sample's window
passes, the demo shows the expiry rejection (proving the expiry check).

## What `granted` proves — and what it doesn't

A granted attestation proves, cryptographically and offline, that **the subject
wallet satisfied the condition** at a recent block — signed by InsumerAPI,
verifiable with the public key alone.

It is a **bearer** proof: it binds the *subject wallet* (`sub`), not whoever
presents it. Exactly like `skyfire-kya`, presenter-binding — proving the agent in
*this* session controls that wallet — is the job of the surrounding ACK session /
DID-auth layer (the same layer the native path below routes through). Treat
`granted: true` as *"a valid wallet-state attestation exists for this wallet,
inside its 30-minute window"* and compose it with your agent-session auth. A
holder-of-key (`cnf`) proof-of-possession binding is the upgrade path if you need
the attestation itself to be non-replayable.

## Two integration paths

1. **Adapter (this demo) — works today, no fork.** Verify the attestation against
the public JWKS and gate on the boolean, the way `skyfire-kya` does.
2. **Native verifier — an upstream option.** ACK's `verifyParsedCredential()`
resolves the issuer through a DID resolver and expects a `JwtProof2020` proof.
To flow natively through that path, InsumerAPI would publish a
`did:web:api.insumermodel.com` document and emit VC-shaped output; then
`getWalletStateClaimVerifier()` (in `src/insumer-ack-id.ts`) slots into
`verifyParsedCredential({ verifiers })` alongside ACK's own claim verifiers.

## Boundary

This demo is a **consumer of InsumerAPI's documented public surface** only
(`openapi.yaml` + the public JWKS). `conditionHash` is treated as an opaque
fingerprint and never recomputed. How state is sourced, how conditions are
evaluated, and how attestations are signed all stay server-side.

It is in production today gating settlement for a Circle Alliance member building
payments for the agentic economy.
31 changes: 31 additions & 0 deletions demos/insumer-wallet-state/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@demos/insumer-wallet-state",
"version": "0.0.1",
"private": true,
"homepage": "https://github.com/agentcommercekit/ack#readme",
"bugs": "https://github.com/agentcommercekit/ack/issues",
"license": "MIT",
"author": {
"name": "InsumerAPI"
},
"repository": {
"type": "git",
"url": "git+https://github.com/agentcommercekit/ack.git",
"directory": "demos/insumer-wallet-state"
},
"type": "module",
"scripts": {
"check:types": "tsc --noEmit",
"clean": "git clean -fdX .turbo",
"demo": "tsx ./src/index.ts",
"test": "vitest"
},
"dependencies": {
"@repo/cli-tools": "workspace:*",
"agentcommercekit": "workspace:*",
"insumer-verify": "1.5.1"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*"
}
}
5 changes: 5 additions & 0 deletions demos/insumer-wallet-state/sample-attestation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"_note": "Sample from POST /v1/attest (format:jwt), issued 2026-06-15T21:16:21Z and valid until 2026-06-15T21:46:21Z — attestations live 30 minutes by design. Run after that window and the demo demonstrates the expiry rejection; set INSUMER_API_KEY to always mint a fresh one. Verifiable with the public JWKS at https://insumermodel.com/.well-known/jwks.json — no secret required.",
"kid": "insumer-attest-v2",
"jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imluc3VtZXItYXR0ZXN0LXYyIn0.eyJwYXNzIjp0cnVlLCJjb25kaXRpb25IYXNoIjpbIjB4ZTVjZGRhZmY4NjI2ZjY4MjE0ZTI1N2IwZmM5NDY1MmQ3NGNlMzI0NWFhNGMzZGIyN2ZkZDY4MWZhOThjMjMzYiJdLCJibG9ja051bWJlciI6IjB4MmQzMDc2MSIsImJsb2NrVGltZXN0YW1wIjoiMjAyNi0wNi0xNVQyMToxNjoyMS4wMDBaIiwicmVzdWx0cyI6W3siY29uZGl0aW9uIjowLCJsYWJlbCI6IiIsInR5cGUiOiJ0b2tlbl9iYWxhbmNlIiwiY2hhaW5JZCI6ODQ1MywibWV0Ijp0cnVlLCJldmFsdWF0ZWRDb25kaXRpb24iOnsidHlwZSI6InRva2VuX2JhbGFuY2UiLCJjaGFpbklkIjo4NDUzLCJjb250cmFjdEFkZHJlc3MiOiJuYXRpdmUiLCJvcGVyYXRvciI6Imd0ZSIsInRocmVzaG9sZCI6IjAuMDAwMSJ9LCJjb25kaXRpb25IYXNoIjoiMHhlNWNkZGFmZjg2MjZmNjgyMTRlMjU3YjBmYzk0NjUyZDc0Y2UzMjQ1YWE0YzNkYjI3ZmRkNjgxZmE5OGMyMzNiIiwiYmxvY2tOdW1iZXIiOiIweDJkMzA3NjEiLCJibG9ja1RpbWVzdGFtcCI6IjIwMjYtMDYtMTVUMjE6MTY6MjEuMDAwWiJ9XSwiaXNzIjoiaHR0cHM6Ly9hcGkuaW5zdW1lcm1vZGVsLmNvbSIsInN1YiI6IjB4ZDhkQTZCRjI2OTY0YUY5RDdlRWQ5ZTAzRTUzNDE1RDM3YUE5NjA0NSIsImp0aSI6IkFUU1QtNjg2N0RDQjc0MDA5QzI2NyIsImlhdCI6MTc4MTU1ODE4MSwiZXhwIjoxNzgxNTU5OTgxfQ.R2P-IBZwdw7pZH3pn54YqIDlaXWvKwNNv5RoqNpEf2oPG0TkTdhmFWOdcWFw4STwpa2WfLFe5vCfDLHGdt1UdQ"
}
81 changes: 81 additions & 0 deletions demos/insumer-wallet-state/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Demo: an ACK-Pay-style service gate backed by an InsumerAPI wallet-state
// attestation.
//
// ACK-Pay grants access on proof a payment cleared (a receipt). Its reference
// server never checks the PAYER's wallet first. This adds the missing step:
// before granting access, require a signed, live wallet-state condition.
//
// With a key: INSUMER_API_KEY=insr_live_... pnpm demo (mints fresh, goes green)
// Without: pnpm demo (verifies the bundled sample)

import { readFile } from "node:fs/promises"
import { colors, errorMessage, log, logJson, successMessage } from "@repo/cli-tools"

import {
INSUMER_ISSUER_DID,
convertInsumerAttestationToVerifiableCredential,
verifyInsumerAttestationAsAckId,
} from "./insumer-ack-id"

const API = "https://api.insumermodel.com"

/** Get an attestation JWT — mint live with INSUMER_API_KEY, else load the bundled sample. */
async function getAttestationJwt(): Promise<string> {
const key = process.env.INSUMER_API_KEY
if (key) {
const res = await fetch(`${API}/v1/attest`, {
method: "POST",
headers: { "X-API-Key": key, "Content-Type": "application/json" },
body: JSON.stringify({
wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
conditions: [
{ type: "token_balance", chainId: 8453, contractAddress: "native", threshold: "0.0001" },
],
format: "jwt",
}),
})
const json = await res.json()
if (!json.ok) throw new Error(JSON.stringify(json.error))
log("• Minted a fresh attestation with your key (1 credit).")
return json.data.jwt
}
const sample = JSON.parse(
await readFile(new URL("../sample-attestation.json", import.meta.url), "utf8"),
)
log("• No INSUMER_API_KEY set — verifying the bundled sample attestation.")
log(colors.dim(" Get a free key: POST https://api.insumermodel.com/v1/keys/create"))
return sample.jwt
}

/** Run the ACK-ID gate against an InsumerAPI wallet-state attestation. */
async function main() {
log("🔐 InsumerAPI wallet-state attestation → ACK-ID gate\n")
log(`This gate checks the PAYER's live on-chain wallet state before access —
the step ACK-Pay's payment-receipt check leaves open.\n`)

const token = await getAttestationJwt()

// trustedIssuers is the service's allowlist — exactly ACK's model.
const trustedIssuers = [INSUMER_ISSUER_DID]
const result = await verifyInsumerAttestationAsAckId(token, trustedIssuers)

log(colors.dim("\nVerification used ONLY the public JWKS — no API key, no secret.\n"))
log(JSON.stringify(result, null, 2))

if (result.granted) {
log(successMessage("\nAccess granted — wallet satisfies the signed, live condition."))
const vc = await convertInsumerAttestationToVerifiableCredential(token)
log("\nSame attestation, as an ACK-compatible W3C Verifiable Credential:")
logJson(vc)
} else if (result.reason?.toLowerCase().includes("expir")) {
log(errorMessage("\nSample expired — attestations are valid 30 min by design."))
log(colors.dim("Set INSUMER_API_KEY and re-run to mint a live one (goes green)."))
} else {
log(errorMessage(`\nAccess denied — ${result.reason ?? "condition not met"}.`))
}
}

main().catch((e: unknown) => {
log(errorMessage((e as Error).message))
process.exit(1)
})
Loading