Skip to main content

Command Palette

Search for a command to run...

Building Human-in-the-Loop Approval for AI Agent Crypto Payments

Updated
6 min read
Building Human-in-the-Loop Approval for AI Agent Crypto Payments

AI agents can now spend crypto on their own. No human clicking approve. That's the point.

But "autonomous" doesn't mean "unsupervised." The question isn't whether your agent can transact — it's whether you'll know when it's about to do something it's never done before, and whether a human can step in before it does.

Spend caps and allow-lists don't answer that. They define what an agent can't do. Nothing watches for when it starts acting differently than it normally does — a transfer to a brand-new address, an amount ten times larger than usual, a contract it's never touched.

This tutorial shows you how to build a human-in-the-loop approval layer for your AI agent wallet in about 15 minutes. When a transaction looks off, your system gets an alert — Slack, email, whatever you use — and a human decides whether it goes through. Built on WalletPrint, an open-source behavioral risk SDK for agent wallets.


What You'll Build

By the end of this tutorial, your agent will:

  • Screen every proposed transaction against its own behavioral history before signing

  • Get back a risk score and plain-English reasons when something looks off

  • Fire a webhook alert to your team (Slack, email, or your own review flow) for medium and high risk transactions

  • Give humans the ability to approve, hold, or escalate — before anything moves

No spend cap changed. No allow-list modified. Just a behavioral signal layer sitting between "agent wants to pay" and "payment goes through."


Prerequisites

  • Node.js 18+

  • An agent using ZeroDev session keys, LangChain tools, or any agent that signs transactions

  • 15 minutes


Step 1 — Install the SDK

npm install @walletprint/sdk

Step 2 — Set Up Your Environment

Add these two lines to your .env file:

WALLETPRINT_API_KEY=walletprint-dev-key
WALLETPRINT_BASE_URL=https://walletprint.up.railway.app

The sandbox key (walletprint-dev-key) works immediately with no signup. Scores are computed live and returned in full — sandbox requests are rate-limited and not persisted, which is fine for testing. When you're ready to build real behavioral baselines, request a production key at walletprint.vercel.app.


Step 3 — Score a Transaction

Here's the most basic usage — no framework-specific wrapper needed:

import { WalletPrintClient } from "@walletprint/sdk";

const client = new WalletPrintClient({
  baseUrl: process.env.WALLETPRINT_BASE_URL!,
  apiKey: process.env.WALLETPRINT_API_KEY!,
});

const result = await client.score({
  wallet: {
    address: "0xYourAgentWallet",
    chain: "base",
  },
  transaction: {
    to: "0xRecipient",
    value_usd: 1200,
    asset: "USDC",
  },
});

console.log(result.score);        // 0-100
console.log(result.band);         // "low" | "medium" | "high"
console.log(result.reason_codes); // plain-English reasons

A response looks like this:

{
  "score": 62,
  "band": "medium",
  "reason_codes": [
    {
      "code": "NEW_RECIPIENT",
      "label": "New recipient",
      "detail": "Wallet has never sent to this address before.",
      "contribution": 15
    },
    {
      "code": "SIZE_EXTREME_OUTLIER",
      "label": "Unusual transaction size",
      "detail": "Transaction value is in the top 1% of this wallet's history.",
      "contribution": 25
    }
  ]
}

No black box. Every flag is explained in plain English, with a contribution score showing how much each reason affected the total.


Step 4 — Set Up Webhook Alerts

Wire up a webhook so your system gets notified the moment a medium or high band transaction is scored. This is the human-in-the-loop layer — when WalletPrint flags something, your team gets an alert and decides what happens next.

curl https://walletprint.up.railway.app/v1/webhook \
  -X PATCH \
  -H "content-type: application/json" \
  -H "x-api-key: YOUR_PRODUCTION_API_KEY" \
  -d '{
    "webhook_url": "https://your-app.com/walletprint/webhook",
    "webhook_bands": ["medium", "high"]
  }'

WalletPrint fires a POST to your URL the moment a flagged transaction is scored — with the full score, band, reason codes, and transaction details. Route it to Slack, email, a human review dashboard, whatever fits your stack. Full webhook payload schema is in the approval flow docs.


Step 5 — Integrate with ZeroDev

If you're using ZeroDev session keys, wrap your sendTransaction call so every transaction is screened before signing:

import { WalletPrintClient, wrapZeroDevSendTransaction } from "@walletprint/sdk";

const client = new WalletPrintClient({
  baseUrl: process.env.WALLETPRINT_BASE_URL!,
  apiKey: process.env.WALLETPRINT_API_KEY!,
});

const screenedSend = wrapZeroDevSendTransaction(
  async (transaction) => sessionKeyClient.sendTransaction(transaction),
  {
    client,
    walletAddress: "0xYourAgentWallet",
    chain: "base",
    getValueUsd: async (tx) => {
      // Replace with your price oracle / token metadata lookup
      return 500;
    },
    onScore: (result) => {
      console.log("WalletPrint:", result.band, result.reason_codes);

      // Your app decides what to do — WalletPrint never blocks automatically
      if (result.band === "high") {
        // pause, alert a human, require manual approval
      }
    },
  }
);

// Use screenedSend exactly like sessionKeyClient.sendTransaction
const { result, score } = await screenedSend({
  to: "0xRecipient",
  value: 1n,
});

The wrapper is advisory only — WalletPrint never blocks a transaction automatically. You get the signal. You decide what happens next.


Step 6 — Integrate with LangChain

If you're building a LangChain agent, add WalletPrint as a tool in your agent's tool list:

import { WalletPrintClient, createLangChainDynamicTool } from "@walletprint/sdk";

const client = new WalletPrintClient({
  baseUrl: process.env.WALLETPRINT_BASE_URL!,
  apiKey: process.env.WALLETPRINT_API_KEY!,
});

const scoreTool = await createLangChainDynamicTool({
  client,
  walletAddress: "0xYourAgentWallet",
  chain: "base",
});

// Add scoreTool to your agent's tool list before signing transactions
const agent = await createReactAgent({
  llm,
  tools: [...yourExistingTools, scoreTool],
});

The agent can now call the score tool before committing to a transaction, and use the result to decide whether to proceed, pause, or escalate.


Step 7 (Optional) — Label Outcomes to Improve the Model

If you know whether a flagged transaction turned out to be legitimate or actually wrong, submit a label. This is how the behavioral model gets better over time:

await client.submitFeedback({
  screened_transaction_id: result.screened_transaction_id,
  label: "false_positive", // or "confirmed_malicious" | "confirmed_benign"
  label_source: "integrator_dashboard",
  notes: "Legitimate treasury transfer to a new partner",
});

Every label you submit makes the model more accurate — not just for your wallet, but across every wallet WalletPrint screens. This is how the cross-wallet clustering signal gets built over time.


What Just Happened

Your agent wallet now has a behavioral layer that didn't exist five minutes ago.

Every transaction gets checked against how that wallet actually behaves — not just whether it's within the rules. First-time recipients get flagged. Unusual amounts get flagged. Velocity spikes get flagged. And when something comes back high band, your system knows about it before anything is signed.

The pattern that drained $285M from Drift would have looked like a perfectly normal transaction to every existing guardrail. This is the layer that notices when something doesn't match the pattern — even when the rules say it's fine.


Resources

Questions or want a production key? walletprint.vercel.app/contact