On this page
Set upTry itWhat each answer meansDenied and review answersCopies and retriesRequest headersWhat the proxy checksLimitsLanes / Proxy URL
Route an agent through the proxy URL
Put the proxy URL in front of the seller's URL. Your x402 client pays as usual, and the guard checks every payment before the seller sees it.
- Lane
- Enforced
- You need
- An API key and proxy URL, an x402 client, and a wallet with test USDC
- Takes
- About 5 minutes
It works with any x402 client in any language. Use the proxy URL or the SDK hook, not both.
Set up
A seller's URL becomes https://vulsight-guard.vercel.app/p/<proxy token>/<seller url>, with the seller's scheme left out. The seller changes nothing and needs no account here.
Copy your proxy URL and export it. It is shown once beside your API key, on Home for your first key and on Keys when you make or rotate one.
export VULSIGHT_PROXY_URL="<your proxy URL>"You see: a URL like https://vulsight-guard.vercel.app/p/vsp_test_.../, ending in a slash.
Install an x402 client.
bun add @x402/fetch @x402/evm viemYou see: the three packages in your package.json.
Make a throwaway wallet in the same shell. Paste its address into the Circle faucet and request test USDC on Base Sepolia.
export EVM_PRIVATE_KEY="$( node --input-type=module -e ' import { generatePrivateKey } from "viem/accounts"; console.log(generatePrivateKey()); ' )" node --input-type=module -e ' import { privateKeyToAccount } from "viem/accounts"; const key = process.env.EVM_PRIVATE_KEY; console.log(privateKeyToAccount(key).address); 'You see: the wallet's address. The key is exported, never printed.
The token in the URL is a secret, and its vsp_test_ prefix does not mean test money. Keep the full URL out of browser history, shared logs and screenshots. If you did not save it, rotate the key on the Keys page for a new one. Rotating replaces the API key too.
Try it
Ask the demo seller for its dataset through the proxy URL.
curl -i "${VULSIGHT_PROXY_URL}vulsight-guard.vercel.app/merchant/dataset"HTTP/2 402
cache-control: no-store
content-type: application/json
payment-required: eyJ4NDAyVmVyc2lvbiI6...
{}That is the seller's own 402, relayed. It makes no decision. The guard decides the retry that carries the signed payment.
This x402 client makes that retry and prints the answer.
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 pay = wrapFetchWithPayment(fetch, client);
// The proxy URL shown with your API key, then the seller's URL
// with its scheme left out.
const proxy = need("VULSIGHT_PROXY_URL");
const seller = "vulsight-guard.vercel.app/merchant/dataset";
const res = await pay(proxy + seller);
// 200 allowed or 200 review_approved with the dataset,
// 403 denied with the reasons, or 402 review_pending.
const status = res.headers.get("guard-status");
console.log(res.status, status, await res.text());Run it with bun agent.ts, or node agent.ts on Node 22.18 or newer with "type": "module" in your package.json. It pays 0.05 test USDC.
HTTP/2 200
cache-control: no-store
content-type: application/json
guard-decision-id: 3f6b2a91-5c0e-4d7a-9b18-2e4f7c6d8a05
guard-settlement-status: reported
guard-status: allowed
payment-response: eyJzdWNjZXNzIjp0cnVlLCJ0cmFuc2FjdGlvbiI6...
{
"name": "product-reviews",
"rows": 10000,
"sample": [...]
}For a denied payment, point it at /merchant/priority (2.50 USDC). Call client.setSpendControls(false) first, since the x402 client refuses a payment over one dollar before the guard sees it.
What each answer means
Read guard-status first. Sent says whether this request's payment left the proxy.
| guard-status | HTTP | Sent | Means | Next step |
|---|---|---|---|---|
allowed | The seller's code | Yes | The guard allowed the payment and the proxy forwarded it. The body is the seller's answer. | Nothing. Read guard-settlement-status for the receipt. |
review_approved | The seller's code | Yes | A person approved the held payment, and the proxy forwarded it. | Treat it as allowed. |
review_pending | 402 | No | The payment is held for a person. The review stays open. | Approve it on Review, then request the URL again and sign the new 402. See Denied and review answers. |
denied | 403 | No | Your policy or the content check refused the payment. The code is payment_denied, and the message carries the reasons. | Read the reasons. Do not pay the seller another way. |
review_denied | 403 | No | A person denied the held payment. | Do not pay it. |
review_expired | 403 | No | Nobody answered before the review timeout. Nothing was paid. | Request the URL again and sign the new 402. The guard decides it as a new payment. |
resign_required | 402 | No | The guard allowed or approved the payment, but its signature ran out before the proxy could forward it. It comes with guard-next-step: fresh_quote. | Request the URL again and sign the new 402. The same decision answers it. |
not_forwarded | 4xx or 5xx | No | The proxy refused this request itself. It did not leave the proxy. | Follow the code's entry on Errors before you re-sign. |
not_enforced | 409 | No | The request asked for enforce mode, and the agent is in observe mode. The code is proxy_not_enforced. | Switch the agent to enforce on Agents, or leave out x-vulsight-require-mode. |
redirected_after_payment | Any | By the earlier request | The seller answered the paid request with a redirect, and this answer is from following it. The payment may already be made. | Read the decision in guard-decision-id before you pay for it again. |
The proxy writes these headers itself. A seller cannot set them.
| Header | On | Means |
|---|---|---|
guard-status | Every answer about a payment | What happened to the payment, from the list above. |
guard-decision-id | Every answer with a decision | The decision's id. Read it on the Decisions page or at GET /api/v1/decisions/{id}. It is the guard's permission, not proof that money moved. |
guard-settlement-status | Every paid answer | confirmed means the Solana network confirmed the transaction. reported means the seller's receipt is on the ledger on the seller's word, as every Base receipt is. unverified means no usable receipt came back, which does not prove that no money moved. Check your wallet, and report a payment that went through with POST /api/v1/settlements. |
guard-next-step | A signature that ran out | fresh_quote. Request the URL again and sign the new 402. |
guard-shadow-status | Observe mode only | What enforce mode would have decided, such as denied or review_pending. The payment went through anyway, with guard-status: allowed. |
Every error code the proxy returns, with its cause and next step, is on Errors. When a seller fails after the payment left, the message says what became of the payment.
Denied and review answers
A denied payment answers 403 with the reasons. Nothing is forwarded.
HTTP/2 403
content-type: application/json
guard-decision-id: 9107cf25-aa34-4d7b-a546-c8b5253634a3
guard-status: denied
{
"error": {
"code": "payment_denied",
"message": "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 across all networks to 2.50 USDC, over the daily cap of 1.00 USDC. 0x894d…4807 is not on your allowlist. Auto-allow is off in your policy."
},
"decisionId": "9107cf25-aa34-4d7b-a546-c8b5253634a3"
}The proxy holds a Base payment for review up to 35 seconds while you decide. Approve in time and your client gets the paid answer. Otherwise it gets this 402, and the review stays open.
HTTP/2 402
cache-control: no-store
content-type: application/json
guard-decision-id: daf814c5-e82b-41d3-8af2-cb71b5ccd6d8
guard-status: review_pending
payment-required: eyJlcnJvciI6IlRoaXMgcGF5bWVu...
retry-after: 0
{
"error": {
"code": "review_pending",
"message": "This payment waits on a review, so it was not forwarded yet. Approve or deny it on the Review page at https://vulsight-guard.vercel.app/review before 2026-09-26 14:02:00 UTC, when the review expires. Then send it again after the retry-after seconds, signed afresh if the signature has expired, and the same review answers it."
},
"decisionId": "daf814c5-e82b-41d3-8af2-cb71b5ccd6d8"
}Approve or deny the payment on Review.
You see: the card moves under Resolved in the last day.
Request the URL again and sign the 402 it returns.
You see: 200 with guard-status review_approved, or 403 with review_denied.
If the review expired first, do step 2 twice.
You see: 403 with review_expired, then a new decision.
Stock x402 clients such as @x402/fetch hand the 402 back to your code and do not retry, so your code sends step 2. Give your client a read timeout of at least 60 seconds. A client that gives up sooner can be charged without getting the paid body.
A Solana payment is not held. It gets the 402 at once, with retry-after: 2. The Reviews page has the buttons, the timeout and how to hold a payment on purpose.
Copies and retries
The proxy knows a payment by what was signed. That is an EVM authorization's payer and nonce, or the Solana transaction the buyer signed.
A copy of the same signed payment, such as a library's automatic retry, reads the decision the first send got. It adds nothing to the day's cap or the hour's count.
| The proxy answers | When the copy arrives |
|---|---|
409 decision_already_used | Its settlement is on file. It paid, so do not send it again. |
403 payment_denied | Its payment was denied, or its review was denied or expired. The answer carries the same reasons. |
| Forwarded | The proxy kept its decision for a retry, after a hold, a brief failure on our side, or a client that hung up. It checks the copy again and forwards it under that decision. An open review answers 402 review_pending again. |
409 duplicate_payment | Anything else, or it was sent after the seller's 402 changed or to another URL. An open review answers this too. |
A payment signed afresh, with a new nonce or a new Solana transaction, is a new attempt. It picks up the decision the proxy kept for the same offer, such as an open or approved review. Otherwise the guard decides it as a new payment.
Never resend a payment that answered duplicate_payment or decision_already_used. Check its decision and your wallet first.
Request headers
The proxy reads these headers on the way to the seller.
| Header | What the proxy does |
|---|---|
PAYMENT-SIGNATURE | The signed x402 v2 payment. The proxy checks it and forwards it only when the guard allows it. |
X-PAYMENT | An x402 v1 payment. Refused with x402_v1. |
x-vulsight-require-mode | Set to enforce, the proxy answers only while the agent is in enforce mode. Otherwise it answers proxy_not_enforced. The seller never sees this header. |
AuthorizationX-API-KeyApi-KeyX-Auth-TokenX-Access-Token | Sent to the seller. After a redirect off the seller's site, the proxy drops them for the rest of the chain. It marks that URL with ~x/ before the host, so keep the mark. Put a seller key in one of these five. |
Authorization: Bearer vs_test_... | A VulSight key. Refused with key_in_request, since the seller must not receive it. |
CookieRefererX-Forwarded-For | Never sent to the seller. |
What the proxy checks
- The signed payment. The payer, payee and amount come from the signature, and the asset and network from the seller's 402. The terms must be ones that 402 offered. Then your policy decides, as
POST /api/v1/decisionsdoes. - Every answer it relays: the seller's 402, pages the agent fetches through it, and paid answers. A payment waits while an answer from that seller is still being checked. A flagged page holds or blocks payments to that seller.
- A page flagged for the same seller through the SDK hook, the advisory MCP tool or
POST /api/v1/contextholds payments to it for an hour. The proxy does not wait for those checks, so wait for their answer before you pay. - The receipt on the way back. The settlement lands on the ledger without a report from the agent.
- A URL that never answers 402 passes through and makes no decision.
Limits
| Limit | Value |
|---|---|
| Seller URL after the token | 2,000 characters |
| Request body, and each seller answer | 4 MiB |
| Seller answers relayed a UTC day, per account | 256 MiB |
| Seller answers relayed a minute, per agent | 120 |
| Text sent for checking a UTC day, per account | 16 MiB |
| Text checked in one answer | 64 KiB |
| Hold on a Base payment while you review | Up to 35 seconds |
Past the daily text limit, unpaid requests answer 429 until midnight UTC. Past 64 KiB, the agent still gets the whole answer, and the next payment to that seller waits for your review. Payments decided and pages checked count toward the key's rate limit.
- Only traffic through this URL is checked. The same agent calling the seller directly is not.
- The proxy checks, it never signs. Your key stays on your side, and the guard cannot pay for you or claw a payment back.
- The token lands in logs and shell history. Rotate the key on Keys if it leaks, and the old token can no longer pay.
- The proxy takes x402 v2 and the supported payments only.
The rest of the limits are on the security model page.