Warns

Coinbase AgentKit

An agent built on Coinbase AgentKit pays by sending transactions through its wallet provider: a USDC transfer for an invoice, a salary or a supplier. Wrap that wallet provider, and every transaction that leaves it gets a Warns decision before it is signed. A held or blocked payment never reaches the wallet.

It guards the wallet provider, not particular actions: the ERC-20 action, a custom action, anything that calls sendTransaction goes through the same checks.

Install

Terminal
npm i @warns/sdk @coinbase/agentkit viem

@coinbase/agentkit stays yours: Warns uses the wallet provider you already have. It needs @warns/sdk 0.5.0 or later.

Wrap the wallet provider

agent.ts
import { createGuard } from "@warns/sdk"import { agentkitWallet } from "@warns/sdk/agentkit"import { AgentKit, ViemWalletProvider, erc20ActionProvider } from "@coinbase/agentkit" const guard = createGuard({  apiKey: process.env.WARNS_KEY,  agentId: "payroll",  baseUrl: "https://api.warns.xyz",  rootAddress: "0xA26aa29cF9b429AD85E7a0F7FA6aBd8edE301e8a",}) const wallet = agentkitWallet(guard, new ViemWalletProvider(walletClient)) const agentkit = await AgentKit.from({  walletProvider: wallet,  actionProviders: [erc20ActionProvider()],})

Register the wallet's address on your agent first, on the network it pays on. With a smart wallet, register the smart account's address: that is the one that sends the USDC.

Say who you pay

Name the person or company each payment is for, and why. Warns remembers the addresses each payee was paid at: a known payee arriving with a new address is held for review, and so is a first payment to any address you never paid.

agent.ts
import { withIntent } from "@warns/sdk/agentkit" await withIntent({ payee: "Alice", purpose: "invoice INV-0042" }, () =>  agent.run("Pay invoice INV-0042"),)

withIntent covers every transfer made inside it. For a rule that does not depend on the task, pass intent when you wrap the wallet: it receives the decoded transfer (recipient, amount in USDC) and returns the payee and purpose.

The payee comes from your agent, so from whatever it read: Warns uses it to group addresses, never as proof of who owns one.

What goes through

TransactionWhat happens
USDC transferA Warns decision first. Allowed: sent once. Held or blocked: never sent.
USDC approve, transferFrom or any other callRefused
ETHRefused, unless you pass allowNativeTransfers: true
Another contractRefused, unless it is in allowContracts
Deploying a contractRefused
Signing a raw hashRefused, unless you pass allowRawSign: true
Permit and payment authorization signaturesRefused
Signing a message, reading the chainGoes through

A refused or blocked payment throws an error your agent sees as text: it says the payment is blocked, or waits for a person to approve it. It never suggests another way to pay.

Held for review

A held transfer throws a PaymentReviewRequiredError with the review's id, and an alert goes out. Once someone approves it in Reviews, the same transfer (same recipient, same amount) goes through once, within 15 minutes.

When Warns is unreachable

The wallet fails closed by default: no decision, no transfer. For payroll, keep it that way. The choice is the same as for every payment, in the dashboard or in code: see Outages and fallback.

After the transfer

The wallet reports each transaction to Warns, with its final hash once it is mined (a smart wallet first returns a user operation hash). Warns matches the transfer it sees onchain to the decision, so the agent is not flagged.

A transfer sent with the raw key, outside the wrapped wallet, has no decision behind it: Warns sees it onchain, records it as unguarded, and freezes the agent. See Security.

Networks

USDC on Base, Base Sepolia and Arc testnet. On Base mainnet, Warns runs in monitor mode for now: it records what it would have decided and blocks nothing.

Limits

  • AgentKit's own x402 action signs a payment authorization, which the wrapped wallet refuses: the decision needs the merchant's request in hand. For x402, use guard.x402Fetch.
  • AgentKit for Python is not wrapped. A Python agent can ask for the same decision through the API before it sends a transfer.