Warns

SDK reference

@warns/sdk wraps the x402 client you already use. It runs on Node 20 or later.

Terminal
npm i @warns/sdk viem @x402/evm @x402/fetch

viem, @x402/evm and @x402/fetch are peer dependencies: the guard uses yours rather than bundling a second copy. It is built for the x402 packages 2.26 and viem 2.56 or later.

createGuard

agent.ts
import { createGuard } from "@warns/sdk" const guard = createGuard({  apiKey: process.env.WARNS_KEY!,  agentId: "research-agent",  baseUrl: "https://api.warns.xyz",  rootAddress: "0xA26aa29cF9b429AD85E7a0F7FA6aBd8edE301e8a",})
OptionDefaultWhat it does
apiKeyrequiredYour key from the dashboard. Its environment, Test or Live, comes with it.
agentIdrequiredThe agent id you set in the dashboard.
baseUrlrequiredhttps://api.warns.xyz
rootAddressrequiredThe key that signs decisions, 0xA26aa29cF9b429AD85E7a0F7FA6aBd8edE301e8a. The guard refuses a decision signed by any other key.
fallbackthe dashboard setting, closed by defaultWhat to do when Warns cannot be reached. Wins over the dashboard setting. See Outages and fallback.
maxAuthorizationWindowSeconds120The longest an exact authorization may stay valid. Lower it if you like; Warns never accepts more than 120. Circle Gateway payments follow their own rule.
timeoutMs2000Timeout of each call to Warns, in milliseconds.
onEventnoneCalled with each event.
allowTypedDatanone(primaryType) => boolean: lets chosen EIP-712 types through the signer.
fetchglobalThis.fetchThe fetch used to call Warns. It never reaches the merchants.
gatewayfalseLets the guard pay Circle Gateway options. x402Fetch turns it on when you pass Circle's schemes.

Leave allowTypedData unset unless you know exactly why you need it. A Permit signature can move funds without any decision behind it, which is why the signer refuses them by default.

x402Fetch

guard.x402Fetch(signer, options) returns a fetch that pays x402 requests through the guard. It replaces the fetch you pass to the x402 client: it wraps your signer, registers the exact scheme for Base, Base Sepolia and Arc testnet, and evaluates every 402 before anything is signed. Only the payment option Warns evaluated is handed to the x402 client, so it cannot pick another one.

agent.ts
const pay = guard.x402Fetch(signer, {  intent: (url) => ({ expectedMaxUsd: 0.05, purpose: "market-data" }),}) const res = await pay("https://api.vendor.com/quote")
OptionDefaultWhat it does
intentnone(url) => ({ expectedMaxUsd, purpose }): what your agent meant to buy at this URL.
walletthe signer's addressThe paying wallet. It must be registered on the agent.
networksBase, Base Sepolia and Arc testnetThe networks to pay on, as eip155:8453, eip155:84532 and eip155:5042002.
fetchglobalThis.fetchThe fetch that reaches the merchants.
gatewaynoneCircle's schemes, to pay Circle Gateway options too. See Circle Gateway.

Two options of createGuard matter for Monitor:

OptionDefaultWhat it does
modelearned from the decisions"monitor" or "protect". Only needed before the first decision, or while Warns cannot be reached.
monitorOnlyNetworksBase mainnetNetworks treated as Monitor until Warns says otherwise.

In Monitor, onEvent also receives not_evaluated when a payment went through without a check, and configuration_error when Warns refused the key or the agent. The payment goes on in both cases.

Intent

FieldWhat it is
expectedMaxUsdThe most your agent expects to pay. Above it, the decision gets AMOUNT_OVER_INTENT.
purposeWhy, in a few words, up to 256 characters. Shown in the dashboard, and policies can match on it.
merchantHint, toolCallIdAccepted and kept with the decision, for your own records.

The intent is optional. Without it, Warns still checks the merchant, the destination, the authorization window, and the amount against your caps and budgets.

wrapFetch and wrapSigner

x402Fetch is made of two pieces you can also use on their own, if you build the x402 client yourself.

  • guard.wrapFetch(fetch, { intent, wallet }) evaluates every 402 that goes through the fetch you give it, and only lets a signed payment out if it matches an approval.
  • guard.wrapSigner(signer) returns a signer that only signs an EIP-3009 TransferWithAuthorization matching an approved payment, field by field. It refuses Permit and Permit2 typed data, and any typed data for Circle Gateway's contracts other than an approved payment. Other typed data passes through unchanged.

Wrap the signer whenever you hand it to code you do not control: a payment that goes around the guarded fetch still cannot be signed.

Circle Gateway

Circle Gateway pays x402 requests in batches, from a USDC balance deposited in advance. To pay Gateway options through the guard, pass Circle's schemes:

agent.ts
import { BatchEvmScheme, CompositeEvmScheme } from "@circle-fin/x402-batching/client" const pay = guard.x402Fetch(signer, {  gateway: { BatchEvmScheme, CompositeEvmScheme },})

Then the guard:

  • picks a Gateway option only when it names Circle's GatewayWallet for its network, and has Warns decide it like any other payment;
  • signs the Gateway authorization only for the approved payment, field by field, valid between 3 days and 7 days and 1 hour: Gateway settles in batches and refuses shorter windows;
  • refuses every other signature for Gateway's contracts, like a withdrawal: it could move the Gateway balance without a decision.

Without the gateway option, a Gateway option is never picked.

Deposits

A deposit moves USDC from the agent's wallet to Gateway. Made outside the guard, it is a payment with no decision behind it, and the agent is frozen. Make it through the guard instead:

agent.ts
import { GatewayClient } from "@circle-fin/x402-batching/client" const gateway = new GatewayClient({ chain: "arcTestnet", privateKey }) await guard.gatewayDeposit({  network: "eip155:5042002",  amount: "5",  deposit: async () => (await gateway.deposit("5")).depositTxHash,})

Warns checks the deposit before deposit runs: 50 USD at most by default, more if a policy sets limits.gatewayDeposit. A blocked or held deposit throws, and nothing is deposited.

flushFallbacks

guard.flushFallbacks() uploads the payments made on fallback during an outage, and resolves with how many went through. The guard also does it on its own after the next answer from Warns, so you rarely need to call it.

Settlements

After a paid request, the guard reports the result to Warns on its own: the transaction, and whether it succeeded. That is how budgets are committed, and how Warns learns a merchant's usual address and price. A failed report never fails your agent's request.

Events

EventFieldsWhen
decisiondecisionEvery decision received from Warns.
fallback_allowamountUsd, reasonA payment let through while Warns was unreachable.
fallback_uploadedactionIdA fallback payment reached Warns.
settlement_reporteddecisionId, successThe result of a payment was recorded.
settlement_report_faileddecisionId, errorIt could not be. Harmless: reconciliation finds the payment onchain.
agent.ts
const guard = createGuard({  // ...  onEvent: (e) => {    if (e.type === "decision") console.log(e.decision.outcome, e.decision.reasons)  },})

Errors

The guard throws rather than paying, so a refusal can never look like a success. Every error extends WarnsError and has a code.

ErrorCodeWhat happened
PaymentBlockedErrorPAYMENT_BLOCKEDRefused, nothing signed. reasons says why. decision is set when Warns decided, and null when the guard refused on its own: an unsupported payment option, a malformed request, a rate limit.
PaymentReviewRequiredErrorPAYMENT_REVIEW_REQUIREDHeld for a human. reviewId identifies it. See Reviews and alerts.
GuardConfigurationErrorfrom the APIA wrong key, an unknown agent or a missing scope. status is the HTTP status. A setup mistake, not a dangerous payment.
GuardUnavailableErrorGUARD_UNAVAILABLEWarns could not be reached and your fallback rules do not cover this payment.
BindingViolationErrorBINDING_VIOLATIONSomething tried to sign or send a payment that no decision approved.
agent.ts
import { PaymentBlockedError, PaymentReviewRequiredError } from "@warns/sdk" try {  await pay(url)} catch (e) {  if (e instanceof PaymentReviewRequiredError) {    // tell a human, then retry the same request once approved  } else if (e instanceof PaymentBlockedError) {    console.log(e.reasons.map((r) => r.message))  } else {    throw e  }}

The decision

What onEvent and the errors carry:

FieldWhat it is
decisionIdThe decision's id, also shown in the dashboard.
outcomeallow, review or block.
enforcementenforced in Protect mode, shadow in Monitor mode.
reasonsA list of { code, message }. See the codes.
score, severityThe risk score from 0 to 100, and low, medium, high or critical.
reviewIdSet when the payment is held.
expiresAtAn allowed payment must be signed before this time, one minute after the decision.