Skip to main content

Documentation Index

Fetch the complete documentation index at: https://cobo.com/products/agentic-wallet/manual/llms.txt

Use this file to discover all available pages before exploring further.

Use this path when your runtime is TypeScript-first and built with Mastra. Run CLI Setup and TypeScript SDK first, then add the Mastra layer. CAW does not ship a dedicated Mastra adapter. The recommended pattern is to define Mastra tools that call @cobo/agentic-wallet, then expose only the subset your agent needs.

Why Mastra fits CAW

Mastra is a good fit when you want:
  • TypeScript-native agents and workflows
  • explicit tool definitions with structured schemas
  • clean separation between planning logic and deterministic execution
That maps well to CAW’s model of scoped authorization, execution, and audit.

Step 1: Install

npm install @cobo/agentic-wallet @mastra/core @ai-sdk/openai zod

Step 2: Configure environment

export AGENT_WALLET_API_URL=https://api.agenticwallet.cobo.com
export AGENT_WALLET_API_KEY=your-api-key
export OPENAI_API_KEY=your-openai-api-key

Step 3: Define CAW-backed Mastra tools

mastra_quickstart.ts
import { randomUUID } from 'node:crypto';

import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

import {
  AuditApi,
  Configuration,
  PactsApi,
  TransactionRecordsApi,
  TransactionsApi,
} from '@cobo/agentic-wallet';

// ─── Env ─────────────────────────────────────────────────────────────────────

function requireEnv(name: string): string {
  const v = process.env[name];
  if (!v) throw new Error(`Missing required environment variable: ${name}`);
  return v;
}

const env = {
  basePath: requireEnv('AGENT_WALLET_API_URL'),
  ownerKey: requireEnv('AGENT_WALLET_API_KEY'),
  walletId: requireEnv('AGENT_WALLET_WALLET_ID'),
  openaiApiKey: requireEnv('OPENAI_API_KEY'),
  destination: process.env.CAW_DESTINATION ?? '0x1111111111111111111111111111111111111111',
};

// ─── Owner-scoped API clients ────────────────────────────────────────────────

const ownerConfig = new Configuration({ apiKey: env.ownerKey, basePath: env.basePath });
const pactsApi = new PactsApi(ownerConfig);
const ownerTxApi = new TransactionsApi(ownerConfig);
const recordsApi = new TransactionRecordsApi(ownerConfig);
const auditApi = new AuditApi(ownerConfig);

// ─── Demo pact spec ──────────────────────────────────────────────────────────
// Allow SETH transfers up to 0.002; anything larger is denied by policy.

const CHAIN_ID = 'SETH';
const TOKEN_ID = 'SETH';
const DEMO_PACT_SPEC = {
  policies: [
    {
      name: 'max-tx-limit',
      type: 'transfer',
      rules: {
        effect: 'allow',
        when: {
          chain_in: [CHAIN_ID],
          token_in: [{ chain_id: CHAIN_ID, token_id: TOKEN_ID }],
        },
        deny_if: { amount_gt: '0.002' },
      },
    },
  ],
  completion_conditions: [{ type: 'time_elapsed', threshold: '86400' }],
};

// ─── Pact session store ──────────────────────────────────────────────────────
// Capture (pact_id → api_key) as submit_pact / get_pact responses come back,
// then resolve pact-scoped TransactionsApi clients at transfer time.

const pactApiKeys = new Map<string, string>();

function capturePact(response: unknown): void {
  if (!response || typeof response !== 'object') return;
  const r = response as Record<string, unknown>;
  const pactId = (r.pact_id ?? r.id) as string | undefined;
  const apiKey = r.api_key as string | undefined;
  if (pactId && apiKey) pactApiKeys.set(pactId, apiKey);
}

function txApiForPact(pactId?: string): TransactionsApi {
  if (!pactId) return ownerTxApi;
  const apiKey = pactApiKeys.get(pactId);
  if (!apiKey) throw new Error(`Unknown pact_id ${pactId}. Call submit_pact or get_pact first.`);
  return new TransactionsApi(new Configuration({ apiKey, basePath: env.basePath }));
}

// ─── Denial handling ─────────────────────────────────────────────────────────
// Surface policy denials back to the LLM as `{ error, suggestion }` so it can
// self-correct, instead of aborting the agent loop with an exception.

async function catchPolicyDenial<T>(
  work: () => Promise<T>,
): Promise<T | { error: unknown; suggestion?: string }> {
  try {
    return await work();
  } catch (err) {
    const data = (err as { response?: { data?: { error?: unknown; suggestion?: string } } })
      ?.response?.data;
    return { error: data?.error ?? 'UNKNOWN_ERROR', suggestion: data?.suggestion };
  }
}

// ─── Prompts ─────────────────────────────────────────────────────────────────

const DEMO_USER_PROMPT =
  `Use wallet ${env.walletId}. ` +
  `Submit a pact for a controlled transfer task and wait until it is active. ` +
  `Using the newly created pact, transfer 0.001 ${TOKEN_ID} to ${env.destination} on ${CHAIN_ID}. ` +
  `Next, using the same pact, attempt 0.005 ${TOKEN_ID}. If denied, follow the denial ` +
  `guidance and retry with a compliant amount. ` +
  `Track the result by request_id and summarize what happened.`;

// ─── Tool definitions (@mastra/core-specific) ────────────────────────────────

const submitPactTool = createTool({
  id: 'submit_pact',
  description: 'Submit a pact and return the pact id.',
  inputSchema: z.object({ wallet_id: z.string(), intent: z.string() }),
  execute: async (input) => {
    const { data } = await pactsApi.submitPact({
      wallet_id: input.wallet_id,
      intent: input.intent,
      spec: DEMO_PACT_SPEC,
    });
    capturePact(data.result);
    return data.result;
  },
});

const getPactTool = createTool({
  id: 'get_pact',
  description: 'Fetch a pact, including its status and api_key once active.',
  inputSchema: z.object({ pact_id: z.string() }),
  execute: async (input) => {
    const { data } = await pactsApi.getPact(input.pact_id);
    capturePact(data.result);
    return data.result;
  },
});

const estimateTransferFeeTool = createTool({
  id: 'estimate_transfer_fee',
  description: 'Estimate fees for a token transfer before submitting it.',
  inputSchema: z.object({
    wallet_uuid: z.string(),
    dst_addr: z.string(),
    token_id: z.string(),
    amount: z.string(),
    pact_id: z.string().optional(),
  }),
  execute: async (input) => {
    const { data } = await txApiForPact(input.pact_id).estimateTransferFee(input.wallet_uuid, {
      chain_id: CHAIN_ID,
      dst_addr: input.dst_addr,
      token_id: input.token_id,
      amount: input.amount,
    });
    return data.result;
  },
});

const transferTool = createTool({
  id: 'transfer_tokens',
  description:
    'Execute a policy-enforced transfer. A unique request_id is auto-generated ' +
    'and returned; use that value to track or look up the tx later. Pass pact_id ' +
    'to invoke under pact-scoped policy permissions.',
  inputSchema: z.object({
    wallet_uuid: z.string(),
    dst_addr: z.string(),
    token_id: z.string(),
    amount: z.string(),
    pact_id: z.string().optional(),
  }),
  execute: async (input) =>
    catchPolicyDenial(async () => {
      const { data } = await txApiForPact(input.pact_id).transferTokens(input.wallet_uuid, {
        chain_id: CHAIN_ID,
        dst_addr: input.dst_addr,
        token_id: input.token_id,
        amount: input.amount,
        request_id: randomUUID(),
      });
      return data.result;
    }),
});

const recordTool = createTool({
  id: 'get_transaction_record_by_request_id',
  description: 'Look up a transaction record by request id.',
  inputSchema: z.object({ wallet_uuid: z.string(), request_id: z.string() }),
  execute: async (input) => {
    const { data } = await recordsApi.getUserTransactionByRequestId(
      input.wallet_uuid,
      input.request_id,
    );
    return data.result;
  },
});

const auditTool = createTool({
  id: 'get_audit_logs',
  description: 'List recent audit log entries for the wallet.',
  inputSchema: z.object({
    wallet_id: z.string(),
    limit: z.number().int().positive().optional(),
  }),
  execute: async (input) => {
    const { data } = await auditApi.listAuditLogs(
      input.wallet_id,
      undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
      input.limit ?? 20,
    );
    const items = (data.result as { items?: Array<{ result?: string }> })?.items ?? [];
    return {
      items,
      allowed: items.filter(it => it.result === 'allowed').length,
      denied: items.filter(it => it.result === 'denied').length,
    };
  },
});

// ─── Boot ────────────────────────────────────────────────────────────────────

const agent = new Agent({
  id: 'cobo-operator',
  name: 'cobo-operator',
  model: openai('gpt-4.1-mini'),
  instructions:
    'Submit a pact before execution, wait until it is active, and follow any denial ' +
    'guidance by retrying inside the allowed boundary.',
  tools: {
    submitPactTool,
    getPactTool,
    estimateTransferFeeTool,
    transferTool,
    recordTool,
    auditTool,
  },
});

const output = await agent.generate(DEMO_USER_PROMPT, { maxSteps: 20 });
console.log(output.text);
For Mastra agents, start with one of these subsets:
  • Execution agentsubmit_pact, one execution tool, one tracking tool
  • Observer agent — balances, transaction records, audit logs
  • Planner plus executor — keep CAW submission in the executor only
Add audit-log tools on the observer side once you want agents to explain operator history. That avoids giving every agent broad wallet authority.

Design guidance

  • keep planner and executor roles separate when possible
  • keep CAW execution in a narrow subset instead of a broad wallet surface
  • track outcomes with transaction records and audit logs
  • treat pact submission as part of the runtime contract, not optional setup