SDK reference
@warns/sdk wraps the x402 client you already use. It runs on Node 20 or later.
npm i @warns/sdk viem @x402/evm @x402/fetchviem, @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
import { createGuard } from "@warns/sdk" const guard = createGuard({ apiKey: process.env.WARNS_KEY!, agentId: "research-agent", baseUrl: "https://api.warns.xyz", rootAddress: "0xA26aa29cF9b429AD85E7a0F7FA6aBd8edE301e8a",})
| Option | Default | What it does |
|---|---|---|
apiKey | required | Your key from the dashboard. Its environment, Test or Live, comes with it. |
agentId | required | The agent id you set in the dashboard. |
baseUrl | required | https://api.warns.xyz |
rootAddress | required | The key that signs decisions, 0xA26aa29cF9b429AD85E7a0F7FA6aBd8edE301e8a. The guard refuses a decision signed by any other key. |
fallback | the dashboard setting, closed by default | What to do when Warns cannot be reached. Wins over the dashboard setting. See Outages and fallback. |
maxAuthorizationWindowSeconds | 120 | The 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. |
timeoutMs | 2000 | Timeout of each call to Warns, in milliseconds. |
onEvent | none | Called with each event. |
allowTypedData | none | (primaryType) => boolean: lets chosen EIP-712 types through the signer. |
fetch | globalThis.fetch | The fetch used to call Warns. It never reaches the merchants. |
gateway | false | Lets 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.
const pay = guard.x402Fetch(signer, { intent: (url) => ({ expectedMaxUsd: 0.05, purpose: "market-data" }),}) const res = await pay("https://api.vendor.com/quote")
| Option | Default | What it does |
|---|---|---|
intent | none | (url) => ({ expectedMaxUsd, purpose }): what your agent meant to buy at this URL. |
wallet | the signer's address | The paying wallet. It must be registered on the agent. |
networks | Base, Base Sepolia and Arc testnet | The networks to pay on, as eip155:8453, eip155:84532 and eip155:5042002. |
fetch | globalThis.fetch | The fetch that reaches the merchants. |
gateway | none | Circle's schemes, to pay Circle Gateway options too. See Circle Gateway. |
Two options of createGuard matter for Monitor:
| Option | Default | What it does |
|---|---|---|
mode | learned from the decisions | "monitor" or "protect". Only needed before the first decision, or while Warns cannot be reached. |
monitorOnlyNetworks | Base mainnet | Networks 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
| Field | What it is |
|---|---|
expectedMaxUsd | The most your agent expects to pay. Above it, the decision gets AMOUNT_OVER_INTENT. |
purpose | Why, in a few words, up to 256 characters. Shown in the dashboard, and policies can match on it. |
merchantHint, toolCallId | Accepted 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-3009TransferWithAuthorizationmatching an approved payment, field by field. It refusesPermitandPermit2typed 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:
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:
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
| Event | Fields | When |
|---|---|---|
decision | decision | Every decision received from Warns. |
fallback_allow | amountUsd, reason | A payment let through while Warns was unreachable. |
fallback_uploaded | actionId | A fallback payment reached Warns. |
settlement_reported | decisionId, success | The result of a payment was recorded. |
settlement_report_failed | decisionId, error | It could not be. Harmless: reconciliation finds the payment onchain. |
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.
| Error | Code | What happened |
|---|---|---|
PaymentBlockedError | PAYMENT_BLOCKED | Refused, 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. |
PaymentReviewRequiredError | PAYMENT_REVIEW_REQUIRED | Held for a human. reviewId identifies it. See Reviews and alerts. |
GuardConfigurationError | from the API | A wrong key, an unknown agent or a missing scope. status is the HTTP status. A setup mistake, not a dangerous payment. |
GuardUnavailableError | GUARD_UNAVAILABLE | Warns could not be reached and your fallback rules do not cover this payment. |
BindingViolationError | BINDING_VIOLATION | Something tried to sign or send a payment that no decision approved. |
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:
| Field | What it is |
|---|---|
decisionId | The decision's id, also shown in the dashboard. |
outcome | allow, review or block. |
enforcement | enforced in Protect mode, shadow in Monitor mode. |
reasons | A list of { code, message }. See the codes. |
score, severity | The risk score from 0 to 100, and low, medium, high or critical. |
reviewId | Set when the payment is held. |
expiresAt | An allowed payment must be signed before this time, one minute after the decision. |