Skip to main content

Choose your integration

Pick the path that matches how you build:

Claude Code

One command. Auto-intercepts all wallet transactions.

OpenClaw

Install from ClawHub. Three tools, safety-net hook.

TypeScript SDK

Full control. MandateClient + MandateWallet.

CLI

Validate from scripts, CI, or MCP server mode.

Python / Any Language

REST API. Works with Python, Go, Rust, or any HTTP client.

Claude Code Plugin

The fastest path. Two commands, zero code changes. The plugin intercepts all wallet transactions Claude executes and validates them against your policy. Step 1: Add the marketplace and install the plugin:
/plugin marketplace add SwiftAdviser/claude-mandate-plugin
/plugin install mandate@mandate
Step 2: Register your agent:
npx @mandate.md/cli login --name "my-claude-agent"
Claim at the printed URL, then set policies at app.mandate.md. The plugin gates every financial tool call. No valid Mandate token, no transaction.
  • Agent calls mandate validate with action + reason
  • If allowed: plugin records a 15-minute token, transaction proceeds
  • If blocked: agent gets counter-evidence explaining why, stops voluntarily
  • If no validation attempted: plugin blocks the tool call entirely
  • Auto-scan on startup finds unprotected wallet calls in your project
  • Works with any wallet: Bankr, MCP payment tools, direct RPC, custom CLIs
GitHub: SwiftAdviser/claude-mandate-plugin

Full Claude Code guide

Two-phase enforcement, gate triggers, token TTLs, and advanced configuration.

OpenClaw Plugin

Install from ClawHub or npm. Your agent gets three Mandate tools automatically.
openclaw plugin install @mandate.md/mandate-openclaw-plugin
The plugin adds these tools to your agent:
ToolWhat it does
mandate_registerRegister agent, get runtime key
mandate_validateCheck transaction against policy
mandate_statusPoll intent state
A safety-net hook blocks any financial tool call that skips mandate_validate. GitHub: SwiftAdviser/mandate-openclaw-plugin ClawHub: clawhub.ai/swiftadviser/mandate

Full OpenClaw guide

Tool parameters, safety-net hook details, and configuration options.

TypeScript SDK

For custom agents. Full control over the validate, sign, broadcast flow.

Install

bun add @mandate.md/sdk

Register your agent

import { MandateClient } from '@mandate.md/sdk';

const { runtimeKey, claimUrl } = await MandateClient.register({
  name: 'my-trading-agent',
  evmAddress: '0xYourAgentWalletAddress',
  chainId: 84532, // Base Sepolia
});

// Save runtimeKey securely. Share claimUrl with the wallet owner.
Store the runtimeKey in your .env file (MANDATE_RUNTIME_KEY=mndt_test_...). Never commit it to git. Share the claimUrl with the wallet owner to link the agent to the dashboard.

Test validation (no wallet needed)

You can test the policy engine without a private key or wallet. This is the fastest way to verify your setup works.
// validate-test.ts — Run: bun run validate-test.ts
import { MandateClient, PolicyBlockedError, ApprovalRequiredError } from '@mandate.md/sdk';

const client = new MandateClient({
  runtimeKey: process.env.MANDATE_RUNTIME_KEY!,
});

try {
  const result = await client.validate({
    action: 'transfer',
    amount: '50',
    to: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
    token: 'USDC',
    reason: 'Payment for API access - invoice #1234',
  });
  console.log('Allowed:', result.allowed);
  console.log('Intent ID:', result.intentId);
} catch (err) {
  if (err instanceof PolicyBlockedError) {
    console.log('Blocked:', err.blockReason, err.declineMessage);
  } else if (err instanceof ApprovalRequiredError) {
    console.log('Needs approval:', err.intentId);
    const status = await client.waitForApproval(err.intentId);
    console.log('Decision:', status.status);
  }
}
Expected output:
  Allowed: true
  Intent ID: a1b2c3d4-...

Execute the transfer

MandateWallet handles the full flow: validate, sign locally, broadcast, confirm. Your private key never leaves your machine.
validate() accepts amounts in USD (e.g., '50' means $50). wallet.transfer() accepts raw token units (e.g., '5000000' means 5 USDC, because USDC has 6 decimals). The policy engine always evaluates in USD.
import { MandateWallet } from '@mandate.md/sdk';

const wallet = new MandateWallet({
  runtimeKey: process.env.MANDATE_RUNTIME_KEY!,
  privateKey: process.env.AGENT_PRIVATE_KEY! as `0x${string}`,
  chainId: 84532,
});

const { txHash, intentId } = await wallet.transfer(
  '0xRecipientAddress',
  '5000000', // 5 USDC (6 decimals)
  '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
  { reason: 'Paying vendor for March services' },
);

console.log('Transaction:', txHash);
console.log('Intent ID:', intentId);
Expected output:
  Transaction: 0x7a8b9c...
  Intent ID: e5f6g7h8-...
// quickstart.ts — Run: bun run quickstart.ts
// Prerequisites: bun add @mandate.md/sdk viem
// Set MANDATE_RUNTIME_KEY and AGENT_PRIVATE_KEY in .env

import { MandateWallet, MandateClient, USDC, CHAIN_ID } from '@mandate.md/sdk';

const RECIPIENT = '0x0000000000000000000000000000000000000001' as `0x${string}`;

async function main() {
  // Step 1: Register (skip if you already have a runtimeKey)
  let runtimeKey = process.env.MANDATE_RUNTIME_KEY;

  if (!runtimeKey) {
    console.log('No runtimeKey found. Registering new agent...');
    const reg = await MandateClient.register({
      name: 'QuickstartAgent',
      evmAddress: '0xYourWalletAddress' as `0x${string}`,
      chainId: CHAIN_ID.BASE_SEPOLIA,
    });
    runtimeKey = reg.runtimeKey;
    console.log(`Registered! runtimeKey: ${runtimeKey}`);
    console.log(`Claim URL: ${reg.claimUrl}`);
    console.log('Save runtimeKey to .env as MANDATE_RUNTIME_KEY');
  }

  // Step 2: Create MandateWallet
  const wallet = new MandateWallet({
    runtimeKey,
    privateKey: process.env.AGENT_PRIVATE_KEY as `0x${string}`,
    chainId: CHAIN_ID.BASE_SEPOLIA,
  });

  // Step 3: Transfer with policy enforcement
  try {
    const { txHash, intentId, status } = await wallet.transfer(
      RECIPIENT,
      '1000000', // 1 USDC (6 decimals)
      USDC.BASE_SEPOLIA,
    );
    console.log('Transfer successful!');
    console.log('  txHash:', txHash);
    console.log('  intentId:', intentId);
    console.log('  status:', status.status);
  } catch (err: unknown) {
    if (err && typeof err === 'object' && 'blockReason' in err) {
      console.error('Blocked:', (err as { blockReason: string }).blockReason);
    } else {
      throw err;
    }
  }
}

main().catch(console.error);
npm: @mandate.md/sdk

CLI

Run directly with npx. No install needed.

Register

npx @mandate.md/cli login --name "my-agent"

Validate

npx @mandate.md/cli validate \
  --action transfer \
  --amount 50 \
  --to 0x036CbD53842c5426634e7929541eC2318f3dCF7e \
  --token USDC \
  --reason "Payment for API access"

Scan your codebase

Find unprotected wallet calls before they reach production:
npx @mandate.md/cli scan ./src
The scanner detects sendTransaction, transfer, approve, and other financial calls missing Mandate validation. Exit code 1 if any found. Use it in CI.

MCP server mode

Turn the CLI into an MCP server for AI assistants:
npx @mandate.md/cli --mcp
npm: @mandate.md/cli

Full CLI reference

All 10 commands, flags, credential storage, and MCP configuration.

Python / Any Language

No SDK needed. Call the REST API directly with any HTTP client.

Register

import requests

resp = requests.post("https://app.mandate.md/api/agents/register", json={
    "name": "my-python-agent",
    "evmAddress": "0xYourAgentWalletAddress",
    "chainId": 84532,
})
data = resp.json()
runtime_key = data["runtimeKey"]
print(f"Runtime key: {runtime_key}")
print(f"Claim URL: {data['claimUrl']}")
# Save runtime_key to .env

Validate

import os, requests

headers = {
    "Authorization": f"Bearer {os.environ['MANDATE_RUNTIME_KEY']}",
    "Content-Type": "application/json",
}

resp = requests.post("https://app.mandate.md/api/validate", headers=headers, json={
    "action": "transfer",
    "amount": "50",
    "to": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    "token": "USDC",
    "reason": "Payment for API access - invoice #1234",
})

data = resp.json()
if resp.status_code == 200 and data.get("allowed"):
    print(f"Allowed. Intent: {data['intentId']}")
elif resp.status_code == 422:
    print(f"Blocked: {data['blockReason']} - {data.get('blockDetail')}")
elif resp.status_code == 202:
    print(f"Needs approval. Intent: {data['intentId']}")
Expected output:
  Allowed. Intent: a1b2c3d4-...
This pattern works with Python, Go, Rust, Ruby, or any language with an HTTP client. See the REST API reference for all endpoints.

Default policy

After registration, every agent starts with:
RuleDefault
Per-transaction limit$100
Daily limit$1,000
Address restrictionsNone
Approval requiredNo
Schedule24/7
Customize in the Dashboard policy builder.

What happens under the hood

  1. Validate. Your agent sends action + reason to Mandate. The policy engine runs 14 checks.
  2. Sign locally. After validation passes, the agent signs with its own key. Mandate never sees it.
  3. Broadcast. Signed transaction goes to the chain.
  4. Verify. Mandate confirms the on-chain tx matches what was validated. Mismatch trips the circuit breaker.

Resources

ResourceLink
Dashboardapp.mandate.md
Documentationdocs.mandate.md
SDK (npm)@mandate.md/sdk
CLI (npm)@mandate.md/cli
Claude Code PluginGitHub
OpenClaw PluginClawHub / GitHub
Skill (SKILL.md)GitHub
Developer CommunityTelegram

Next Steps

How It Works

Validation flow, intent lifecycle, and state machine.

Handle Errors

PolicyBlockedError, CircuitBreakerError, and 12 block reason codes.

Handle Approvals

Wait for human approval with polling, timeouts, and callbacks.

All Integrations

GOAT SDK, AgentKit, ElizaOS, GAME, ACP, MCP Server, and more.