SDK for TypeScript
On this page
bun add @vulsight/ guard @x402/ fetch @x402/ evm viemThe SDK accepts any 2.x release of @x402/core from 2.24 on, so the install line above carries one copy of it beside the x402 client. The same checks run through the API from any other language.
What it is
Two hooks on the x402 client you already have, plus one fetch wrapper for the pages the agent reads. Before a payment is signed the hook sends the seller's 402 (payee, amount, asset, network, resource URL) and the page the agent read to the guard, and acts on the answer. Each attempt carries its own Idempotency-Key, and when the guard was down or the connection dropped the request is sent once more under it, so the payment is decided once. A request that ran out the hook's own time budget is not sent again.
- Allowed. The payment is signed, tagged with its decision id.
- Denied. The payment is never signed, and
wrapFetchWithPaymentthrows with the reasons. - Review. The hook waits for a person, then proceeds on an approval. When the wait runs out it throws without paying, and the review stays open on the guard. Pick Approve and always allow this payee, and the agent's next payment to that seller skips the first-payment review.
What it costs you to add
One guard(client, options) call and one wrap. Order matters: the guard's fetch goes inside and wrapFetchWithPayment outside, so the guard sees the paid retry. Nothing else in your agent changes. The wallet key stays in your process, and the API key is the one secret that goes to the guard.
mode says what happens when the guard cannot answer in time. enforce blocks the payment and says the guard was unreachable, observe lets it through and logs the gap. A wrong API key blocks the payment in both modes.
Since @x402/core 2.23.0 the client refuses any payment over one dollar by default before the guard's hook runs, so keep that cap or call .setSpendControls(false) on the client to make the guard's policy the only limit.
Run it against the demo seller
import { guard } from "@vulsight/ guard";
import { ExactEvmScheme } from "@x402/ evm/ exact/ client";
import { wrapFetchWithPayment, x402Client } from "@x402/ fetch";
import { privateKeyToAccount } from "viem/ accounts";
const need = (name: string) => {
const value = process.env[name];
if (!value) throw new Error(`Set ${name} before starting the agent.`);
return value;
};
const key = need("EVM_PRIVATE_KEY") as `0x${string}`;
const account = privateKeyToAccount(key);
const client = new x402Client().register("eip155:*", new ExactEvmScheme(account));
const vulsight = guard(client, {
apiKey: need("VULSIGHT_API_KEY"),
baseUrl: "https:// vulsight-guard.vercel.app",
});
// files the pages the agent reads
const read = vulsight.fetch(fetch);
// checks every payment before it is signed
const pay = wrapFetchWithPayment(read, client);
const url = "https:// vulsight-guard.vercel.app/ merchant/ dataset";
await read(url, { headers: { accept: "text/ html" } });
// 0.05 test USDC on Base Sepolia
const paid = await pay(url);
await vulsight.reportSettlement(paid);EVM_PRIVATE_KEY is the hex private key of a throwaway wallet made for this. Create one with cast wallet new or viem's generatePrivateKey, paste its address into the Circle faucet (USDC, Base Sepolia), and never point the snippet at a wallet that holds real funds.
vulsight.fetch hashes the first part of each page the agent reads and files it with the guard once per page, then returns the response untouched. vulsight.reportSettlement reads the receipt off the paid response and records the transaction, so the ledger shows the money that moved.
Read everything the agent reads
vulsight.fetch files the pages the agent fetches. A file, a search result, or another tool's output never reaches the guard unless you file it. vulsight.file(text, origin) posts it under the same session, once per distinct text and again on every read of one the guard flagged, and answers pass, restrict (flagged), or unavailable. It never throws. A guard it cannot reach, a post it refused, or a check that did not finish all read as unavailable. The next payment is checked on its own; a rejected API key aborts it and says so. origin is a short label such as the tool's name, default tool.
await generateText({
model, tools, prompt,
onStepFinish: async ({ toolResults }) => {
for (const r of toolResults) await vulsight.file(JSON.stringify(r.output), r.toolName);
},
});Both file tool output only. The user's own turns are never filed: the person is the principal, the check scores untrusted content, and a scan of their instructions would assess their own orders. With content enforcement on, a completed content block denies payments in the session for the next hour, as a blocked page does. An unfinished check needs review. An agent that files faster than the account's scan ceiling gets a 429 the SDK reads as unavailable and moves past. The guard never took that text, so the hook retries the filing before the next payment and an enforce agent blocks that payment if the retry fails too.
Denied and review answers
A denied payment throws before anything is signed. The message carries every sentence the dashboard shows for that decision, the rules that denied it first, then the holds, then the notes, so you can put them in front of a person.
client.setSpendControls(false); // 2.50 is over the client's own one dollar cap
try {
await pay("https:// vulsight-guard.vercel.app/ merchant/ priority"); // 2.50 USDC
} catch (error) {
// Failed to create payment payload: Payment creation aborted: The amount
// 2.50 USDC is over the per-transaction limit of 0.10 USDC. This payment of
// 2.50 USDC would bring today's total to 2.50 USDC, over the daily cap of
// 1.00 USDC. First payment to 0x894d…4807. Auto-allow for first payments is
// off in your policy. The payee 0x894d…4807 is not in your allowlist.
}A payment in review pauses inside the hook while it asks the guard for the answer. An approval goes ahead. If nobody answers within waitForReviewSeconds, the call throws to say it is still waiting on a reviewer and the payment was blocked. The review stays open on the guard. Pick Approve and always allow this payee, and the agent's next payment to that seller skips the first-payment review.
Solana
The hook accepts Solana Mainnet and Solana Devnet x402 payments. Solana needs your own Solana x402 signer. Register the @x402/svm client scheme with it on the x402 client, then attach guard(client, options) before using it. Allow the same network on Policy; its USDC is allowed with it. The example on this page pays on Base Sepolia.
Solana wallet and mint addresses are case-sensitive. Do not lowercase them. The SDK checks before signing; the proxy can inspect a sponsored exact USDC transfer before forwarding. A held payment needs a fresh blockhash after approval; let the x402 client rebuild the transaction instead of reusing an expired one.
Solana support ships in @vulsight/guard 0.4.0; update if you installed earlier.
Its limit
- Only payments made through this client are checked. Another process, another library, or a signature made by hand is not.
- A 402 that arrives through a redirect is refused. The client would retry the URL it asked for with the payment attached, and either URL could take it, so ask for the resource by its final URL. A signed retry is sent with redirects disabled, so a seller that redirects the paid request fails it instead of moving the signature to another URL.
- Only pages read through
vulsight.fetchand text passed tovulsight.fileare filed, and only their first 64 KiB. Anything the agent read some other way is not part of the decision. A text that fills that 64 KiB is filed asunavailablewithout a scan, since the guard cannot tell it from the start of a longer page. - A payment on a network the guard does not know is refused by the SDK itself, before the guard is asked and in both modes. It knows Base Sepolia, Base, Solana Devnet, and Solana Mainnet.
- If the guard cannot be reached in time, an
enforceagent blocks the payment and anobserveagent pays and logs the gap. Pick the one you can live with.
The rest of the limits are on the security model and limits page.