diff --git a/typescript/.changeset/warm-wallets-verify.md b/typescript/.changeset/warm-wallets-verify.md new file mode 100644 index 000000000..0deffb50c --- /dev/null +++ b/typescript/.changeset/warm-wallets-verify.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Added InsumerAPI action provider for on-chain wallet verification and trust profiles diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index e0eccdeca..6c8fd6584 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -15,6 +15,7 @@ export * from "./enso"; export * from "./erc20"; export * from "./erc721"; export * from "./farcaster"; +export * from "./insumer"; export * from "./jupiter"; export * from "./messari"; export * from "./pyth"; diff --git a/typescript/agentkit/src/action-providers/insumer/README.md b/typescript/agentkit/src/action-providers/insumer/README.md new file mode 100644 index 000000000..959ede72c --- /dev/null +++ b/typescript/agentkit/src/action-providers/insumer/README.md @@ -0,0 +1,111 @@ +# Insumer Action Provider + +This directory contains the InsumerAPI action provider implementation, which provides actions for privacy-preserving on-chain wallet verification, trust profiling, and discount code validation across 32 chains (30 EVM + Solana + XRPL). + +## Directory Structure + +``` +insumer/ +├── constants.ts # API base URL and error messages +├── insumerActionProvider.test.ts # Tests for the provider +├── insumerActionProvider.ts # Main provider with InsumerAPI functionality +├── index.ts # Main exports +├── README.md # Documentation +├── schemas.ts # Zod input schemas for all actions +└── types.ts # TypeScript interfaces for API responses +``` + +## Setup + +1. Get a free API key at [insumermodel.com/developers](https://insumermodel.com/developers/) +2. Set the `INSUMER_API_KEY` environment variable, or pass `apiKey` in the config + +```typescript +import { insumerActionProvider } from "@coinbase/agentkit"; + +// Via environment variable +const provider = insumerActionProvider(); + +// Via config +const provider = insumerActionProvider({ apiKey: "insr_live_..." }); +``` + +## Actions + +- `verify_wallet`: Verify on-chain wallet conditions with boolean attestations + + - Checks token balances, NFT ownership, EAS attestations, Farcaster IDs + - Returns ECDSA-signed booleans, never raw balances + - Supports compliance templates (e.g. `coinbase_verified_account`) + - 1 credit per call (2 with Merkle proofs) + +- `get_wallet_trust_profile`: Generate a wallet trust profile + + - 17 checks across 4 dimensions: stablecoins, governance, NFTs, staking + - ECDSA-signed and independently verifiable + - 3 credits per call (6 with Merkle proofs) + +- `get_batch_wallet_trust_profiles`: Batch trust profiles for up to 10 wallets + + - 5-8x faster than sequential calls + - Supports partial success + - 3 credits per wallet + +- `validate_discount_code`: Validate an INSR-XXXXX discount code + + - No API key required (public endpoint) + - Returns validity, discount percentage, and expiration + +- `list_compliance_templates`: List available EAS compliance templates + - No API key required (public endpoint) + - Coinbase Verifications and Gitcoin Passport templates + +## Supported Chains + +30 EVM chains: Ethereum, BNB Chain, Base, Avalanche, Polygon, Arbitrum, Optimism, Chiliz, Soneium, Plume, World Chain, Sonic, Gnosis, Mantle, Scroll, Linea, zkSync Era, Blast, Taiko, Ronin, Celo, Moonbeam, Moonriver, Viction, opBNB, Unichain, Ink, Sei, Berachain, ApeChain. Plus Solana and XRPL (32 total). + +## Pricing + +- Free tier: 100 credits, no credit card required +- 25 credits per 1 USDC ($0.04/credit) +- Attestation: 1 credit (2 with Merkle proofs) +- Trust profile: 3 credits (6 with Merkle proofs) +- Public endpoints (validate_discount_code, list_compliance_templates): free + +## Links + +- [Developer Documentation](https://insumermodel.com/developers/) +- [API Reference](https://insumermodel.com/developers/api-reference/) +- [OpenAPI Spec](https://insumermodel.com/openapi.yaml) +- [MCP Server](https://www.npmjs.com/package/mcp-server-insumer) +- [LangChain Integration](https://pypi.org/project/langchain-insumer/) + +## Examples + +### Verifying Coinbase KYC Status + +``` +Prompt: "Check if wallet 0xd8dA...96045 has Coinbase KYC verification on Base" + +The agent will call verify_wallet with: +- wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" +- conditions: [{ type: "eas_attestation", template: "coinbase_verified_account" }] +``` + +### Getting a Wallet Trust Profile + +``` +Prompt: "What's the trust profile for vitalik.eth?" + +The agent will call get_wallet_trust_profile with: +- wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" +``` + +### Batch Trust Profiling + +``` +Prompt: "Compare trust profiles for these 3 wallets: 0xd8dA..., 0xAb58..., 0x7a25..." + +The agent will call get_batch_wallet_trust_profiles with: +- wallets: [{ wallet: "0xd8dA..." }, { wallet: "0xAb58..." }, { wallet: "0x7a25..." }] +``` diff --git a/typescript/agentkit/src/action-providers/insumer/constants.ts b/typescript/agentkit/src/action-providers/insumer/constants.ts new file mode 100644 index 000000000..138ca0639 --- /dev/null +++ b/typescript/agentkit/src/action-providers/insumer/constants.ts @@ -0,0 +1,10 @@ +/** + * Base URL for the InsumerAPI + */ +export const INSUMER_API_BASE_URL = "https://api.insumermodel.com"; + +/** + * Default error message when API key is missing + */ +export const INSUMER_API_KEY_MISSING_ERROR = + "INSUMER_API_KEY is not configured. Get a free key at https://insumermodel.com/developers/"; diff --git a/typescript/agentkit/src/action-providers/insumer/index.ts b/typescript/agentkit/src/action-providers/insumer/index.ts new file mode 100644 index 000000000..116468889 --- /dev/null +++ b/typescript/agentkit/src/action-providers/insumer/index.ts @@ -0,0 +1,4 @@ +export * from "./insumerActionProvider"; +export * from "./schemas"; +export * from "./types"; +export * from "./constants"; diff --git a/typescript/agentkit/src/action-providers/insumer/insumerActionProvider.test.ts b/typescript/agentkit/src/action-providers/insumer/insumerActionProvider.test.ts new file mode 100644 index 000000000..6f2ee9333 --- /dev/null +++ b/typescript/agentkit/src/action-providers/insumer/insumerActionProvider.test.ts @@ -0,0 +1,444 @@ +import { insumerActionProvider, InsumerActionProvider } from "./insumerActionProvider"; +import { INSUMER_API_KEY_MISSING_ERROR } from "./constants"; + +const MOCK_API_KEY = "insr_live_0123456789abcdef0123456789abcdef01234567"; + +const MOCK_ATTEST_RESPONSE = { + ok: true, + data: { + attestation: { + id: "ATST-A1B2C", + pass: false, + results: [ + { condition: 0, label: "USDC >= 1000", type: "token_balance", met: true, chainId: 1 }, + { condition: 1, label: "Bored Ape holder", type: "nft_ownership", met: false, chainId: 1 }, + ], + passCount: 1, + failCount: 1, + attestedAt: "2026-02-28T12:00:00.000Z", + expiresAt: "2026-02-28T12:30:00.000Z", + }, + sig: "MEYCIQDxABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx", + kid: "insumer-attest-v1", + }, + meta: { + creditsRemaining: 97, + creditsCharged: 1, + version: "1.0", + timestamp: "2026-02-28T12:00:00.000Z", + }, +}; + +const MOCK_TRUST_RESPONSE = { + ok: true, + data: { + trust: { + id: "TRST-A1B2C", + wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + conditionSetVersion: "v1", + dimensions: { + stablecoins: { checks: [], passCount: 3, failCount: 4, total: 7 }, + governance: { checks: [], passCount: 2, failCount: 2, total: 4 }, + nfts: { checks: [], passCount: 0, failCount: 3, total: 3 }, + staking: { checks: [], passCount: 1, failCount: 2, total: 3 }, + }, + summary: { + totalChecks: 17, + totalPassed: 6, + totalFailed: 11, + dimensionsWithActivity: 3, + dimensionsChecked: 4, + }, + profiledAt: "2026-02-28T12:00:00.000Z", + expiresAt: "2026-02-28T12:30:00.000Z", + }, + sig: "MEYCIQDxABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx", + kid: "insumer-attest-v1", + }, + meta: { + creditsRemaining: 47, + creditsCharged: 3, + version: "1.0", + timestamp: "2026-02-28T12:00:00.000Z", + }, +}; + +const MOCK_BATCH_TRUST_RESPONSE = { + ok: true, + data: { + results: [ + { + trust: { + id: "TRST-A1B2C", + wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + conditionSetVersion: "v1", + dimensions: { + stablecoins: { checks: [], passCount: 3, failCount: 4, total: 7 }, + governance: { checks: [], passCount: 2, failCount: 2, total: 4 }, + nfts: { checks: [], passCount: 0, failCount: 3, total: 3 }, + staking: { checks: [], passCount: 1, failCount: 2, total: 3 }, + }, + summary: { + totalChecks: 17, + totalPassed: 6, + totalFailed: 11, + dimensionsWithActivity: 3, + dimensionsChecked: 4, + }, + profiledAt: "2026-02-28T12:00:00.000Z", + expiresAt: "2026-02-28T12:30:00.000Z", + }, + sig: "MEYCIQDx...", + kid: "insumer-attest-v1", + }, + { + error: { + wallet: "not-a-valid-address", + message: "Invalid wallet address", + }, + }, + ], + summary: { requested: 2, succeeded: 1, failed: 1 }, + }, + meta: { + creditsRemaining: 44, + creditsCharged: 3, + version: "1.0", + timestamp: "2026-02-28T12:00:00.000Z", + }, +}; + +const MOCK_CODE_VALID_RESPONSE = { + ok: true, + data: { + valid: true, + code: "INSR-A7K3M", + merchantId: "merchant123", + discountPercent: 10, + expiresAt: "2026-03-01T00:00:00.000Z", + createdAt: "2026-02-28T12:00:00.000Z", + }, + meta: { version: "1.0", timestamp: "2026-02-28T12:00:00.000Z" }, +}; + +const MOCK_CODE_INVALID_RESPONSE = { + ok: true, + data: { + valid: false, + code: "INSR-ZZZZZ", + reason: "not_found", + }, + meta: { version: "1.0", timestamp: "2026-02-28T12:00:00.000Z" }, +}; + +const MOCK_TEMPLATES_RESPONSE = { + ok: true, + data: { + templates: { + coinbase_verified_account: { + provider: "Coinbase", + description: "Coinbase Verified Account", + chainId: 8453, + chainName: "Base", + }, + coinbase_verified_country: { + provider: "Coinbase", + description: "Coinbase Verified Country", + chainId: 8453, + chainName: "Base", + }, + }, + }, + meta: { version: "1.0", timestamp: "2026-02-28T12:00:00.000Z" }, +}; + +const MOCK_ERROR_RESPONSE = { + ok: false, + error: { code: 402, message: "Insufficient verification credits" }, +}; + +describe("InsumerActionProvider", () => { + let provider: InsumerActionProvider; + + beforeEach(() => { + provider = insumerActionProvider({ apiKey: MOCK_API_KEY }); + jest.restoreAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + delete process.env.INSUMER_API_KEY; + }); + + describe("constructor", () => { + it("should initialize with API key from config", () => { + const customProvider = insumerActionProvider({ apiKey: "custom-key" }); + expect(customProvider["apiKey"]).toBe("custom-key"); + }); + + it("should initialize with API key from environment variable", () => { + process.env.INSUMER_API_KEY = "env-key"; + const envProvider = insumerActionProvider(); + expect(envProvider["apiKey"]).toBe("env-key"); + }); + + it("should throw error if API key is not provided", () => { + delete process.env.INSUMER_API_KEY; + expect(() => insumerActionProvider()).toThrow(INSUMER_API_KEY_MISSING_ERROR); + }); + }); + + describe("verifyWallet", () => { + it("should successfully verify wallet conditions", async () => { + const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_ATTEST_RESPONSE, + } as Response); + + const response = await provider.verifyWallet({ + wallet: "0x1234567890abcdef1234567890abcdef12345678", + conditions: [ + { type: "token_balance", contractAddress: "0xA0b8...", chainId: 1, threshold: 1000 }, + ], + }); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain("/v1/attest"); + expect((options as RequestInit).headers).toEqual( + expect.objectContaining({ "X-API-Key": MOCK_API_KEY }), + ); + expect(response).toContain("ATST-A1B2C"); + expect(response).toContain("SOME CONDITIONS FAILED"); + expect(response).toContain("USDC >= 1000: PASS"); + expect(response).toContain("Bored Ape holder: FAIL"); + }); + + it("should handle API error response", async () => { + jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_ERROR_RESPONSE, + } as Response); + + const response = await provider.verifyWallet({ + wallet: "0x1234567890abcdef1234567890abcdef12345678", + conditions: [{ type: "token_balance", contractAddress: "0xA0b8...", chainId: 1 }], + }); + + expect(response).toContain("InsumerAPI error"); + expect(response).toContain("Insufficient verification credits"); + }); + + it("should handle network error", async () => { + jest.spyOn(global, "fetch").mockRejectedValue(new Error("Network error")); + + const response = await provider.verifyWallet({ + wallet: "0x1234567890abcdef1234567890abcdef12345678", + conditions: [{ type: "token_balance", contractAddress: "0xA0b8...", chainId: 1 }], + }); + + expect(response).toContain("Error verifying wallet"); + expect(response).toContain("Network error"); + }); + }); + + describe("getWalletTrustProfile", () => { + it("should successfully get trust profile", async () => { + const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_TRUST_RESPONSE, + } as Response); + + const response = await provider.getWalletTrustProfile({ + wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + }); + + expect(fetchMock).toHaveBeenCalled(); + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain("/v1/trust"); + expect(response).toContain("TRST-A1B2C"); + expect(response).toContain("stablecoins: 3/7 passed"); + expect(response).toContain("6/17 checks passed"); + }); + + it("should handle API error response", async () => { + jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_ERROR_RESPONSE, + } as Response); + + const response = await provider.getWalletTrustProfile({ + wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + }); + + expect(response).toContain("InsumerAPI error"); + }); + + it("should handle network error", async () => { + jest.spyOn(global, "fetch").mockRejectedValue(new Error("Connection refused")); + + const response = await provider.getWalletTrustProfile({ + wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + }); + + expect(response).toContain("Error getting trust profile"); + expect(response).toContain("Connection refused"); + }); + }); + + describe("getBatchWalletTrustProfiles", () => { + it("should successfully get batch trust profiles", async () => { + const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_BATCH_TRUST_RESPONSE, + } as Response); + + const response = await provider.getBatchWalletTrustProfiles({ + wallets: [ + { wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }, + { wallet: "not-a-valid-address" }, + ], + }); + + expect(fetchMock).toHaveBeenCalled(); + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain("/v1/trust/batch"); + expect(response).toContain("1/2 succeeded"); + expect(response).toContain("1 failed"); + expect(response).toContain("6/17 checks passed"); + expect(response).toContain("ERROR"); + }); + + it("should handle API error response", async () => { + jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_ERROR_RESPONSE, + } as Response); + + const response = await provider.getBatchWalletTrustProfiles({ + wallets: [{ wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }], + }); + + expect(response).toContain("InsumerAPI error"); + }); + + it("should handle network error", async () => { + jest.spyOn(global, "fetch").mockRejectedValue(new Error("Timeout")); + + const response = await provider.getBatchWalletTrustProfiles({ + wallets: [{ wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }], + }); + + expect(response).toContain("Error getting batch trust profiles"); + }); + }); + + describe("validateDiscountCode", () => { + it("should successfully validate a valid code", async () => { + const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_CODE_VALID_RESPONSE, + } as Response); + + const response = await provider.validateDiscountCode({ code: "INSR-A7K3M" }); + + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toContain("/v1/codes/INSR-A7K3M"); + expect((options as RequestInit).headers).not.toEqual( + expect.objectContaining({ "X-API-Key": expect.anything() }), + ); + expect(response).toContain("Valid: YES"); + expect(response).toContain("Discount: 10%"); + }); + + it("should handle an invalid code", async () => { + jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_CODE_INVALID_RESPONSE, + } as Response); + + const response = await provider.validateDiscountCode({ code: "INSR-ZZZZZ" }); + + expect(response).toContain("Valid: NO"); + expect(response).toContain("Reason: not_found"); + }); + + it("should not send API key for public endpoint", async () => { + const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_CODE_VALID_RESPONSE, + } as Response); + + await provider.validateDiscountCode({ code: "INSR-A7K3M" }); + + const headers = (fetchMock.mock.calls[0][1] as RequestInit).headers as Record; + expect(headers["X-API-Key"]).toBeUndefined(); + }); + + it("should handle network error", async () => { + jest.spyOn(global, "fetch").mockRejectedValue(new Error("Network error")); + + const response = await provider.validateDiscountCode({ code: "INSR-A7K3M" }); + + expect(response).toContain("Error validating code"); + }); + }); + + describe("listComplianceTemplates", () => { + it("should successfully list templates", async () => { + const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_TEMPLATES_RESPONSE, + } as Response); + + const response = await provider.listComplianceTemplates({}); + + expect(fetchMock).toHaveBeenCalled(); + const [url] = fetchMock.mock.calls[0]; + expect(url).toContain("/v1/compliance/templates"); + expect(response).toContain("coinbase_verified_account"); + expect(response).toContain("Coinbase Verified Account"); + expect(response).toContain("Base"); + }); + + it("should not send API key for public endpoint", async () => { + const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_TEMPLATES_RESPONSE, + } as Response); + + await provider.listComplianceTemplates({}); + + const headers = (fetchMock.mock.calls[0][1] as RequestInit).headers as Record; + expect(headers["X-API-Key"]).toBeUndefined(); + }); + + it("should handle API error response", async () => { + jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_ERROR_RESPONSE, + } as Response); + + const response = await provider.listComplianceTemplates({}); + + expect(response).toContain("InsumerAPI error"); + }); + + it("should handle network error", async () => { + jest.spyOn(global, "fetch").mockRejectedValue(new Error("DNS resolution failed")); + + const response = await provider.listComplianceTemplates({}); + + expect(response).toContain("Error listing templates"); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for all networks", () => { + expect(provider.supportsNetwork({ protocolFamily: "evm" })).toBe(true); + expect(provider.supportsNetwork({ protocolFamily: "solana" })).toBe(true); + expect(provider.supportsNetwork({ protocolFamily: "unknown" })).toBe(true); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/insumer/insumerActionProvider.ts b/typescript/agentkit/src/action-providers/insumer/insumerActionProvider.ts new file mode 100644 index 000000000..efaf07c4b --- /dev/null +++ b/typescript/agentkit/src/action-providers/insumer/insumerActionProvider.ts @@ -0,0 +1,391 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { + VerifyWalletSchema, + GetWalletTrustProfileSchema, + GetBatchWalletTrustProfilesSchema, + ValidateDiscountCodeSchema, + ListComplianceTemplatesSchema, +} from "./schemas"; +import { INSUMER_API_BASE_URL, INSUMER_API_KEY_MISSING_ERROR } from "./constants"; +import { + InsumerActionProviderConfig, + InsumerResponse, + AttestationData, + TrustProfileData, + BatchTrustData, + CodeValidationData, + ComplianceTemplatesData, +} from "./types"; + +/** + * InsumerActionProvider is an action provider for InsumerAPI interactions. + * It enables AI agents to verify on-chain wallet conditions, generate trust profiles, + * and validate discount codes across 32 chains (30 EVM + Solana + XRPL). + * + * @augments ActionProvider + */ +export class InsumerActionProvider extends ActionProvider { + private readonly apiKey: string; + + /** + * Constructor for the InsumerActionProvider class. + * + * @param config - The configuration options for the InsumerActionProvider + */ + constructor(config: InsumerActionProviderConfig = {}) { + super("insumer", []); + + config.apiKey ||= process.env.INSUMER_API_KEY; + + if (!config.apiKey) { + throw new Error(INSUMER_API_KEY_MISSING_ERROR); + } + + this.apiKey = config.apiKey; + } + + /** + * Verifies on-chain wallet conditions. Returns ECDSA-signed boolean attestations + * for token balances, NFT ownership, EAS attestations, and Farcaster IDs + * across 32 chains (30 EVM + Solana + XRPL). Never exposes raw wallet balances. + * + * @param args - The verification parameters + * @returns A formatted string with verification results + */ + @CreateAction({ + name: "verify_wallet", + description: `Verify on-chain wallet conditions with privacy-preserving boolean attestations. +It takes the following inputs: +- wallet: EVM wallet address (0x...) +- conditions: Array of 1-10 conditions to check (token_balance, nft_ownership, eas_attestation, farcaster_id) +- solanaWallet: Optional Solana wallet address for Solana conditions +- proof: Optional "merkle" for EIP-1186 Merkle storage proofs + +Important notes: +- Returns ECDSA-signed boolean results, never raw balances +- Supports 32 chains (30 EVM + Solana + XRPL) +- Each condition specifies its own chainId +- Use compliance templates (e.g. coinbase_verified_account) for EAS attestations +- Costs 1 credit per call (2 with proof="merkle") +- Results include a cryptographic signature verifiable with npm install insumer-verify`, + schema: VerifyWalletSchema, + }) + async verifyWallet(args: z.infer): Promise { + try { + const result = await this.apiRequest("POST", "/v1/attest", { + ...(args.wallet ? { wallet: args.wallet } : {}), + conditions: args.conditions, + ...(args.proof ? { proof: args.proof } : {}), + ...(args.solanaWallet ? { solanaWallet: args.solanaWallet } : {}), + ...(args.xrplWallet ? { xrplWallet: args.xrplWallet } : {}), + ...(args.format ? { format: args.format } : {}), + }); + + if (!result.ok) { + return `InsumerAPI error: ${result.error.message}`; + } + + const { attestation, sig } = result.data; + const conditionResults = attestation.results + .map(r => ` - ${r.label || "condition"}: ${r.met ? "PASS" : "FAIL"}`) + .join("\n"); + + return [ + `Wallet Verification Result (${attestation.id}):`, + `Overall: ${attestation.pass ? "ALL CONDITIONS MET" : "SOME CONDITIONS FAILED"}`, + `Conditions:`, + conditionResults, + `Signature: ${sig.substring(0, 20)}...`, + `Expires: ${attestation.expiresAt}`, + result.meta.creditsRemaining !== undefined + ? `Credits remaining: ${result.meta.creditsRemaining}` + : "", + ] + .filter(Boolean) + .join("\n"); + } catch (error: unknown) { + return `Error verifying wallet: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Generates an ECDSA-signed wallet trust profile with 17 checks across + * 4 dimensions: stablecoins, governance, NFTs, and staking. + * + * @param args - The trust profile parameters + * @returns A formatted string with the trust profile + */ + @CreateAction({ + name: "get_wallet_trust_profile", + description: `Generate an ECDSA-signed wallet trust profile with 17 on-chain checks across 4 dimensions. +It takes the following inputs: +- wallet: EVM wallet address (0x...) to profile +- solanaWallet: Optional Solana wallet address to include Solana USDC check +- proof: Optional "merkle" for EIP-1186 Merkle storage proofs + +Important notes: +- Checks 4 dimensions: stablecoins (7 checks), governance (4), NFTs (3), staking (3) +- Returns per-check booleans and dimensional summaries +- Results are ECDSA-signed and independently verifiable +- Costs 3 credits (6 with proof="merkle") +- Trust profiles expire after 30 minutes`, + schema: GetWalletTrustProfileSchema, + }) + async getWalletTrustProfile(args: z.infer): Promise { + try { + const result = await this.apiRequest("POST", "/v1/trust", { + wallet: args.wallet, + ...(args.solanaWallet ? { solanaWallet: args.solanaWallet } : {}), + ...(args.xrplWallet ? { xrplWallet: args.xrplWallet } : {}), + ...(args.proof ? { proof: args.proof } : {}), + }); + + if (!result.ok) { + return `InsumerAPI error: ${result.error.message}`; + } + + const { trust, sig } = result.data; + const dimensionLines = Object.entries(trust.dimensions) + .map(([name, dim]) => ` ${name}: ${dim.passCount}/${dim.total} passed`) + .join("\n"); + + return [ + `Wallet Trust Profile (${trust.id}):`, + `Wallet: ${trust.wallet}`, + `Dimensions:`, + dimensionLines, + `Summary: ${trust.summary.totalPassed}/${trust.summary.totalChecks} checks passed across ${trust.summary.dimensionsWithActivity}/${trust.summary.dimensionsChecked} active dimensions`, + `Signature: ${sig.substring(0, 20)}...`, + `Expires: ${trust.expiresAt}`, + result.meta.creditsRemaining !== undefined + ? `Credits remaining: ${result.meta.creditsRemaining}` + : "", + ] + .filter(Boolean) + .join("\n"); + } catch (error: unknown) { + return `Error getting trust profile: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Generates trust profiles for up to 10 wallets in a single batch request. + * 5-8x faster than sequential calls with shared block fetches. + * + * @param args - The batch trust profile parameters + * @returns A formatted string with batch results + */ + @CreateAction({ + name: "get_batch_wallet_trust_profiles", + description: `Generate trust profiles for up to 10 wallets in a single batch request. +It takes the following inputs: +- wallets: Array of 1-10 wallet entries, each with a wallet address and optional solanaWallet +- proof: Optional "merkle" for Merkle storage proofs on all wallets + +Important notes: +- 5-8x faster than sequential calls due to shared block fetches +- Each wallet gets an independent ECDSA-signed profile +- Supports partial success (some wallets may fail while others succeed) +- Costs 3 credits per wallet (6 with proof="merkle")`, + schema: GetBatchWalletTrustProfilesSchema, + }) + async getBatchWalletTrustProfiles( + args: z.infer, + ): Promise { + try { + const result = await this.apiRequest("POST", "/v1/trust/batch", { + wallets: args.wallets, + ...(args.proof ? { proof: args.proof } : {}), + }); + + if (!result.ok) { + return `InsumerAPI error: ${result.error.message}`; + } + + const { results, summary } = result.data; + const walletLines = results + .map(r => { + if ("error" in r) { + return ` - ${r.error.wallet}: ERROR - ${r.error.message}`; + } + return ` - ${r.trust.wallet} (${r.trust.id}): ${r.trust.summary.totalPassed}/${r.trust.summary.totalChecks} checks passed`; + }) + .join("\n"); + + return [ + `Batch Trust Profiles:`, + `${summary.succeeded}/${summary.requested} succeeded${summary.failed > 0 ? `, ${summary.failed} failed` : ""}`, + `Results:`, + walletLines, + result.meta.creditsRemaining !== undefined + ? `Credits remaining: ${result.meta.creditsRemaining}` + : "", + ] + .filter(Boolean) + .join("\n"); + } catch (error: unknown) { + return `Error getting batch trust profiles: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Validates a discount code (INSR-XXXXX format) for merchant checkout flows. + * No authentication required. + * + * @param args - The code validation parameters + * @returns A formatted string with validation results + */ + @CreateAction({ + name: "validate_discount_code", + description: `Validate an InsumerAPI discount code (INSR-XXXXX format). +It takes the following inputs: +- code: The discount code to validate (format: INSR-XXXXX) + +Important notes: +- No API key required (public endpoint) +- Returns validity status, discount percentage, and expiration +- Invalid codes include a reason (expired, already_used, not_found) +- Used in merchant checkout flows to apply on-chain verification discounts`, + schema: ValidateDiscountCodeSchema, + }) + async validateDiscountCode(args: z.infer): Promise { + try { + const result = await this.apiRequest( + "GET", + `/v1/codes/${encodeURIComponent(args.code)}`, + undefined, + false, + ); + + if (!result.ok) { + return `InsumerAPI error: ${result.error.message}`; + } + + const { data } = result; + + if (data.valid) { + return [ + `Discount Code Validation:`, + `Code: ${data.code}`, + `Valid: YES`, + `Discount: ${data.discountPercent}%`, + `Merchant: ${data.merchantId}`, + `Expires: ${data.expiresAt}`, + ].join("\n"); + } + + return [ + `Discount Code Validation:`, + `Code: ${data.code}`, + `Valid: NO`, + `Reason: ${data.reason}`, + ].join("\n"); + } catch (error: unknown) { + return `Error validating code: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Lists available compliance templates for EAS attestation verification. + * Templates abstract away raw EAS schema IDs and attester addresses. + * No authentication required. + * + * @param _args - Empty input object (no parameters needed) + * @returns A formatted string with available templates + */ + @CreateAction({ + name: "list_compliance_templates", + description: `List available compliance templates for EAS attestation verification. +It takes no inputs. + +Important notes: +- No API key required (public endpoint) +- Templates simplify EAS attestation checks by abstracting schema IDs and attester addresses +- Use template names in verify_wallet conditions instead of raw schemaId/attester +- Currently includes Coinbase Verifications (KYC, country, Coinbase One) and Gitcoin Passport on Base/Optimism`, + schema: ListComplianceTemplatesSchema, + }) + async listComplianceTemplates( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _args: z.infer, + ): Promise { + try { + const result = await this.apiRequest( + "GET", + "/v1/compliance/templates", + undefined, + false, + ); + + if (!result.ok) { + return `InsumerAPI error: ${result.error.message}`; + } + + const templateLines = Object.entries(result.data.templates) + .map( + ([name, tmpl]) => + ` - ${name}: ${tmpl.description} (${tmpl.provider}, ${tmpl.chainName})`, + ) + .join("\n"); + + return [`Available Compliance Templates:`, templateLines].join("\n"); + } catch (error: unknown) { + return `Error listing templates: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Checks if the InsumerAPI action provider supports the given network. + * InsumerAPI is multi-chain (30 EVM + Solana + XRPL = 32 chains), so this always returns true. + * + * @param _ - The network to check + * @returns Always returns true as InsumerAPI supports all networks + */ + supportsNetwork(_: Network): boolean { + return true; + } + + /** + * Makes a request to the InsumerAPI. + * + * @param method - HTTP method + * @param path - API path (e.g. "/v1/attest") + * @param body - Optional request body for POST requests + * @param authenticated - Whether to include the API key header (default: true) + * @returns The parsed API response + */ + private async apiRequest( + method: "GET" | "POST", + path: string, + body?: Record, + authenticated: boolean = true, + ): Promise> { + const headers: Record = { + "Content-Type": "application/json", + }; + + if (authenticated) { + headers["X-API-Key"] = this.apiKey; + } + + const response = await fetch(`${INSUMER_API_BASE_URL}${path}`, { + method, + headers, + ...(body ? { body: JSON.stringify(body) } : {}), + }); + + return (await response.json()) as InsumerResponse; + } +} + +/** + * Factory function to create a new InsumerActionProvider instance. + * + * @param config - The configuration options for the InsumerActionProvider + * @returns A new instance of InsumerActionProvider + */ +export const insumerActionProvider = (config: InsumerActionProviderConfig = {}) => + new InsumerActionProvider(config); diff --git a/typescript/agentkit/src/action-providers/insumer/schemas.ts b/typescript/agentkit/src/action-providers/insumer/schemas.ts new file mode 100644 index 000000000..4131f72e4 --- /dev/null +++ b/typescript/agentkit/src/action-providers/insumer/schemas.ts @@ -0,0 +1,146 @@ +import { z } from "zod"; + +/** + * Schema for a single verification condition + */ +const ConditionSchema = z + .object({ + type: z + .enum(["token_balance", "nft_ownership", "eas_attestation", "farcaster_id"]) + .describe("The type of on-chain condition to verify"), + contractAddress: z + .string() + .optional() + .describe("Token or NFT contract address (required for token_balance and nft_ownership)"), + chainId: z + .union([z.number(), z.string()]) + .optional() + .describe("EVM chain ID (integer) or 'solana'. Required for token_balance and nft_ownership"), + threshold: z + .number() + .optional() + .describe("Minimum token balance threshold (default: 0, meaning any non-zero balance)"), + decimals: z + .number() + .optional() + .describe("Token decimals for threshold comparison (e.g. 6 for USDC, 18 for most ERC-20s)"), + label: z.string().optional().describe("Human-readable label for this condition"), + template: z + .string() + .optional() + .describe( + "Compliance template name (e.g. coinbase_verified_account). Use list_compliance_templates to see available templates", + ), + schemaId: z + .string() + .optional() + .describe("Raw EAS schema ID (use template instead when possible)"), + attester: z + .string() + .optional() + .describe("EAS attester address (required with schemaId for eas_attestation)"), + }) + .strict(); + +/** + * Input schema for verifying wallet conditions (POST /v1/attest) + */ +export const VerifyWalletSchema = z + .object({ + wallet: z + .string() + .optional() + .describe("EVM wallet address (0x...). Required unless all conditions are Solana-only"), + conditions: z + .array(ConditionSchema) + .min(1) + .max(10) + .describe("Array of 1-10 on-chain conditions to verify"), + proof: z + .enum(["merkle"]) + .optional() + .describe("Set to 'merkle' for EIP-1186 Merkle storage proofs. Costs 2 credits instead of 1"), + solanaWallet: z + .string() + .optional() + .describe("Solana wallet address (base58). Required for Solana conditions"), + xrplWallet: z + .string() + .optional() + .describe("XRPL wallet address (r...). Required for XRPL conditions"), + format: z + .enum(["jwt"]) + .optional() + .describe("Set to 'jwt' to include an ES256-signed JWT (Wallet Auth) in the response"), + }) + .strict(); + +/** + * Input schema for getting a wallet trust profile (POST /v1/trust) + */ +export const GetWalletTrustProfileSchema = z + .object({ + wallet: z + .string() + .describe( + "EVM wallet address (0x...) to profile. Returns 17 checks across 4 dimensions: stablecoins, governance, NFTs, staking", + ), + solanaWallet: z + .string() + .optional() + .describe("Optional Solana wallet address to include Solana USDC check"), + xrplWallet: z + .string() + .optional() + .describe("Optional XRPL wallet address (r...) to include XRPL stablecoin checks"), + proof: z + .enum(["merkle"]) + .optional() + .describe("Set to 'merkle' for EIP-1186 Merkle storage proofs. Costs 6 credits instead of 3"), + }) + .strict(); + +/** + * Input schema for batch wallet trust profiles (POST /v1/trust/batch) + */ +export const GetBatchWalletTrustProfilesSchema = z + .object({ + wallets: z + .array( + z + .object({ + wallet: z.string().describe("EVM wallet address (0x...)"), + solanaWallet: z + .string() + .optional() + .describe("Optional Solana wallet address for this wallet"), + xrplWallet: z + .string() + .optional() + .describe("Optional XRPL wallet address (r...) for this wallet"), + }) + .strict(), + ) + .min(1) + .max(10) + .describe("Array of 1-10 wallet entries to profile. 5-8x faster than sequential calls"), + proof: z + .enum(["merkle"]) + .optional() + .describe("Set to 'merkle' for Merkle storage proofs on all wallets. 6 credits/wallet"), + }) + .strict(); + +/** + * Input schema for validating a discount code (GET /v1/codes/{code}) + */ +export const ValidateDiscountCodeSchema = z + .object({ + code: z.string().describe("Discount code in INSR-XXXXX format"), + }) + .strict(); + +/** + * Input schema for listing compliance templates (GET /v1/compliance/templates) + */ +export const ListComplianceTemplatesSchema = z.object({}).strict(); diff --git a/typescript/agentkit/src/action-providers/insumer/types.ts b/typescript/agentkit/src/action-providers/insumer/types.ts new file mode 100644 index 000000000..e2eecc865 --- /dev/null +++ b/typescript/agentkit/src/action-providers/insumer/types.ts @@ -0,0 +1,147 @@ +/** + * Configuration options for the InsumerActionProvider. + */ +export interface InsumerActionProviderConfig { + /** + * InsumerAPI key (format: insr_live_ followed by 40 hex characters). + * Falls back to INSUMER_API_KEY environment variable if not provided. + */ + apiKey?: string; +} + +/** + * Successful response envelope from InsumerAPI + */ +export interface InsumerSuccessResponse { + ok: true; + data: T; + meta: { + creditsRemaining?: number; + creditsCharged?: number; + version: string; + timestamp: string; + }; +} + +/** + * Error response envelope from InsumerAPI + */ +export interface InsumerErrorResponse { + ok: false; + error: { + code: number; + message: string; + }; +} + +/** + * Union type for any InsumerAPI response + */ +export type InsumerResponse = InsumerSuccessResponse | InsumerErrorResponse; + +/** + * Attestation result from POST /v1/attest + */ +export interface AttestationData { + attestation: { + id: string; + pass: boolean; + results: Array<{ + condition: number; + label?: string; + type: string; + chainId?: number | string; + met: boolean; + evaluatedCondition?: Record; + conditionHash?: string; + blockNumber?: string; + blockTimestamp?: string; + ledgerIndex?: number; + }>; + passCount: number; + failCount: number; + attestedAt: string; + expiresAt: string; + }; + sig: string; + kid: string; + jwt?: string; +} + +/** + * Trust profile result from POST /v1/trust + */ +export interface TrustProfileData { + trust: { + id: string; + wallet: string; + conditionSetVersion: string; + dimensions: Record< + string, + { + checks: Array<{ + label: string; + chainId?: number; + met: boolean; + }>; + passCount: number; + failCount: number; + total: number; + } + >; + summary: { + totalChecks: number; + totalPassed: number; + totalFailed: number; + dimensionsWithActivity: number; + dimensionsChecked: number; + }; + profiledAt: string; + expiresAt: string; + }; + sig: string; + kid: string; +} + +/** + * Batch trust result from POST /v1/trust/batch + */ +export interface BatchTrustData { + results: Array< + | { trust: TrustProfileData["trust"]; sig: string; kid: string } + | { error: { wallet: string; message: string } } + >; + summary: { + requested: number; + succeeded: number; + failed: number; + }; +} + +/** + * Code validation result from GET /v1/codes/{code} + */ +export interface CodeValidationData { + valid: boolean; + code: string; + merchantId?: string; + discountPercent?: number; + expiresAt?: string; + createdAt?: string; + reason?: string; +} + +/** + * Compliance templates result from GET /v1/compliance/templates + */ +export interface ComplianceTemplatesData { + templates: Record< + string, + { + provider: string; + description: string; + chainId: number; + chainName: string; + } + >; +}