Reviews and alerts
In Protect mode, a payment that needs a human is held: nothing is signed until someone approves it.
When a payment is held
Your code gets a PaymentReviewRequiredError with a reviewId, and the payment appears in Reviews with its amount, merchant, destination and the reasons it was held.
import { PaymentReviewRequiredError } from "@warns/sdk" try { await pay(url)} catch (e) { if (e instanceof PaymentReviewRequiredError) { console.log("held for review", e.reviewId) } else { throw e }}
In Monitor mode nothing is held and no review is opened. The dashboard shows the payment as Would hold.
Approve, then retry
- In Reviews, choose Approve once or Deny.
- From your agent, retry the exact same request.
An approval lets that exact payment through once: same agent, wallet, merchant, address and asset, for at most the amount you approved, within 15 minutes. After a denial, a retry opens a new review.
Warns keeps the approval on its side, so the retry can come from a restarted agent, a new serverless invocation or another process. Until someone decides, retries of the same payment stay in one review instead of opening a new one each time.
A payment with a reason that always blocks is never held, so there is nothing to approve. A review stays in the queue until someone decides.
Alerts
Warns tells you when a payment is held for your review, and when an agent is frozen after a payment that went around Warns (see Security model).
An agent in monitor mode is never stopped, so Warns also tells you when it would have held or blocked one of its payments: at most one alert per agent and per hour, the rest is in Transactions. Turn these off in Settings, Alerts if you only want the others.
Set it up in Settings, Alerts: paste a webhook address, click Send a test, then Save. Each alert is sent once to each channel, within a minute. Email alerts are coming soon.
Slack and Discord
- Slack: create an incoming webhook for the channel of your choice, and paste its address.
- Discord: in the channel's settings, open Integrations, then Webhooks, create one and copy its address.
The message gives the amount, the agent, the merchant and the reasons, with a link to the dashboard. Text that comes from a merchant, like the URL your agent requested, stays plain text: it never becomes a link, a mention or a preview.
Any other endpoint
Any public https address that takes JSON works. Warns sends a POST with this body, waits 10 seconds at most and does not follow redirects:
{ "type": "review_pending", "workspace": "Acme", "environment": "live", "agent": "research-agent", "subject": "Payment held for review: 0.25 USDC from research-agent", "text": "Research agent wants to pay 0.25 USDC to 0x2222…2222 for https://api.vendor.com/quote.\nHeld because: api.vendor.com usually gets paid at a different address.\nNothing is signed until you approve it.", "url": "https://app.warns.xyz/reviews", "data": { "reviewId": "…", "reasons": [{ "code": "DESTINATION_CHANGED", "message": "api.vendor.com usually gets paid at a different address" }], "action": { "amountAtomic": "250000", "asset": { "symbol": "USDC", "decimals": 6 }, "destination": { "address": "0x2222222222222222222222222222222222222222" }, "request": { "url": "https://api.vendor.com/quote" } }, "at": "2026-10-03T09:12:00Z" }}
type is review_pending, agent_frozen (with data.frozenBy, data.at and data.mode, monitor or protect), would_block for a monitor decision that would have stopped the payment (with data.outcome, review or block, data.reasons, data.action and data.network), or test when you click Send a test. The action above is shortened: it carries the whole payment as Warns evaluated it.
Only public https addresses are accepted: no IP address, no localhost or private name, no credentials in the address.
Next: Outages and fallback