TypeScript SDK reference
@canopy-ai/sdk is the official TypeScript client. Node 18+. Ships ESM and CJS. Zero runtime dependencies.
Constructor
import { Canopy } from "@canopy-ai/sdk";
const canopy = new Canopy({
apiKey: process.env.CANOPY_API_KEY!, // required
agentId: process.env.CANOPY_AGENT_ID!, // required for pay/preview/check/fetch/discover/ping/budget
baseUrl: "https://trycanopy.ai", // optional
});| Option | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Org API key (ak_live_… or ak_test_…) |
agentId | string | No* | Required for most methods |
baseUrl | string | No | Override the API base URL (local dev) |
pay()
Issues a policy-gated USDC payment. Always returns; never throws on a policy outcome.
canopy.pay(args: PayArgs): Promise<PayResult>
interface PayArgs {
to: string; // 0x… recipient address. Use canopy.fetch(serviceUrl)
// for paid-service interactions.
amountUsd: number; // e.g. 0.10
idempotencyKey?: string; // safe replay key
chainId?: number; // defaults to 8453 (Base)
}
type PayResult =
| { status: "allowed";
txHash: string | null;
signature: string | null;
transactionId: string | null;
costUsd: number | null;
idempotent?: boolean;
dryRun?: boolean; }
| { status: "pending_approval";
approvalId: string;
transactionId: string;
reason: string;
recipientName: string | null;
recipientAddress: string | null;
amountUsd: number | null;
agentName: string | null;
expiresAt: string | null;
chatApprovalEnabled: boolean; }
| { status: "denied";
reason: string;
transactionId: string; };Pass
idempotencyKeywhenever a call might be retried (webhook handlers, framework retries). A second call with the same(agentId, idempotencyKey)returns the cached result without re-charging.
preview()
Dry-runs the policy without signing or persisting. Returns the same PayResult shape with dryRun: true on allowed.
canopy.preview(args: { to: string; amountUsd: number }): Promise<PayResult>check()
URL-driven counterpart to preview(). Probes a paywalled URL, parses the 402 (x402 or MPP), runs the agent's policy in dry-run mode, and returns the parsed offer plus an allowed / pending_approval / denied verdict — without signing.
canopy.check(url: string): Promise<CheckResult>
interface CheckOffer {
rail: "x402" | "mpp";
chainId: number;
amountUsd: number;
recipient: { address: string; slug: string | null; name: string | null };
resourceUrl: string;
scheme: string | null; // x402 only — typically "exact"
network: string; // "base", "tempo", "eip155:8453", …
realm: string | null; // mpp only
cached: boolean; // true when served from the 60s probe cache
}
type CheckResult =
| (CheckOffer & { status: "allowed" })
| (CheckOffer & { status: "pending_approval";
reason: string;
approvalThresholdUsd: number | null })
| (CheckOffer & { status: "denied";
reason: string });Server-side probe — Canopy's egress, not yours. Cached per (org, url) for 60 seconds. Use as the second leg of a discover → check → fetch chain when the LLM should weigh the cost before committing.
fetch()
Drop-in fetch replacement that auto-pays HTTP 402 responses. Non-402 responses pass through.
canopy.fetch(url: string, init?: RequestInit): Promise<Response>getApprovalStatus() / waitForApproval()
canopy.getApprovalStatus(approvalId: string): Promise<ApprovalStatus>
canopy.waitForApproval(
approvalId: string,
options?: { timeoutMs?: number; pollIntervalMs?: number },
): Promise<ApprovalStatus>
interface ApprovalStatus {
status: "pending" | "approved" | "denied" | "expired";
decidedAt: string | null;
expiresAt: string;
transactionId: string;
}waitForApproval polls every 2 seconds by default; default timeout 5 minutes. Throws CanopyApprovalTimeoutError on timeout — the approval is still alive; resume polling with getApprovalStatus.
discover()
Queries Canopy's registry of x402 services. Filtered by the agent's policy.
canopy.discover(opts?: DiscoverArgs): Promise<DiscoveredService[]>
interface DiscoverArgs {
category?: string | string[];
query?: string;
limit?: number; // default 20, max 50
includeBlocked?: boolean; // include policyAllowed: false entries
includeUnverified?: boolean;
}
interface DiscoveredService {
slug: string; // canonical service id, e.g. "openai"
name: string;
description: string | null;
category: string;
logoUrl: string | null;
docsUrl: string | null;
paymentMethods: Array<{ // one entry per supported rail
realm: string;
baseUrl: string;
protocol: "x402" | "mpp-tempo";
}>;
endpoints: Array<{ // catalog-declared endpoints
method: string;
path: string;
description: string | null;
priceAtomic: string | null;
currency: string | null;
pricingModel: string | null;
protocol: string | null;
}>;
preferredBaseUrl: string | null; // rail picked by treasury balance
policyAllowed: boolean;
}ping()
Health check + auto-register the agent in the dashboard. Run on startup to fail fast on bad config.
canopy.ping(): Promise<PingResult>
interface PingResult {
ok: true;
agent: { id: string; name: string | null; status: string; policyId: string | null; policyName: string | null };
org: { name: string | null; treasuryAddress: string };
latencyMs: number;
}budget()
Spend-cap snapshot for the current period.
canopy.budget(): Promise<BudgetSnapshot>
interface BudgetSnapshot {
agentId: string;
capUsd: number | null;
spentUsd: number;
remainingUsd: number | null;
periodHours: number;
periodResetsAt: string | null;
}capUsd and remainingUsd are null when no policy is bound.
getTools()
Returns Canopy's canonical tool list for any LLM-tool framework.
canopy.getTools(): CanopyTool[]
interface CanopyTool {
name: string; // canopy_pay | canopy_discover_services | canopy_approve | canopy_deny
description: string;
parameters: Record<string, unknown>; // JSON Schema
execute: (args: any) => Promise<unknown>;
}Use this when you want the framework-agnostic shape. For one of the supported frameworks, prefer the namespace methods below — they return the right shape directly.
canopy.openai
OpenAI Chat Completions / Responses adapter. No peer dep.
canopy.openai.tools(): OpenAIChatCompletionTool[]
canopy.openai.dispatch(toolCalls: OpenAIToolCall[] | null | undefined): Promise<OpenAIToolMessage[]>tools() returns the [{ type: "function", function: { name, description, parameters } }] shape that goes straight into chat.completions.create({ tools }). dispatch() consumes completion.choices[0].message.tool_calls, runs every Canopy tool call, and returns [{ role: "tool", tool_call_id, content }] messages ready to append for the next turn.
Behavior:
- Skips tool calls naming non-Canopy tools — your host loop dispatches those.
- Errors thrown by Canopy methods are embedded as
{"error": ...}JSON in the tool message so the LLM can react. pending_approvaloutcomes propagate intact — the rich fields (recipientName,amountUsd,expiresAt,chatApprovalEnabled) are preserved in the tool message content.
canopy.anthropic
Anthropic Messages adapter. No peer dep.
canopy.anthropic.tools(): AnthropicTool[]
canopy.anthropic.dispatch(content: AnthropicContentBlock[] | null | undefined): Promise<AnthropicToolResultBlock[]>tools() returns [{ name, description, input_schema }]. dispatch() consumes reply.content, picks out tool_use blocks, and returns [{ type: "tool_result", tool_use_id, content }] blocks that you wrap in a user message for the next turn.
Same skip-and-embed behavior as canopy.openai.dispatch().
For Claude Agent SDK, use Connect Claude Agent SDK and the Canopy MCP server instead. Claude Agent SDK requires MCP tools to be allowed with names like mcp__canopy__canopy_pay or the wildcard mcp__canopy__*.
canopy.vercel
Vercel AI SDK adapter. No peer dep. Vercel runs the dispatch loop itself, so no dispatch() method.
canopy.vercel.tools(): Record<string, { description: string; parameters: Record<string, unknown>; execute: (args: any) => Promise<unknown> }>Pass the result directly as tools on generateText / streamText.
Subpath imports
Adapters that wrap a framework's own class (and therefore need that framework's package) ship as subpath imports. They're optional peer dependencies — installs that don't use them don't pay for them.
import { Canopy } from "@canopy-ai/sdk";
import { toLangChainTools } from "@canopy-ai/sdk/langchain";
const canopy = new Canopy({ apiKey, agentId });
const lcTools = toLangChainTools(canopy); // DynamicStructuredTool[]Install peer dep separately: npm install @langchain/core.
Errors
pay(), preview(), and check() never throw for denied or pending_approval. Other failures throw — see the Errors reference.