Skip to content
On this pageSet upTry itWhat each answer meansDenied and review answersCopies and retriesRequest headersWhat the proxy checksLimits

Lanes / 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.

  1. 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.

    Terminal
    export VULSIGHT_PROXY_URL="<your proxy URL>"

    You see: a URL like https://vulsight-guard.vercel.app/p/vsp_test_.../, ending in a slash.

  2. Install an x402 client.

    Terminal
    bun add @x402/fetch @x402/evm viem

    You see: the three packages in your package.json.

  3. Make a throwaway wallet in the same shell. Paste its address into the Circle faucet and request test USDC on Base Sepolia.

    Terminal
    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.

Terminal
curl -i "${VULSIGHT_PROXY_URL}vulsight-guard.vercel.app/merchant/dataset"
Answer
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.

agent.ts
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.

Allowed
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-statusHTTPSentMeansNext step
allowedThe seller's codeYesThe 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_approvedThe seller's codeYesA person approved the held payment, and the proxy forwarded it.Treat it as allowed.
review_pending402NoThe 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.
denied403NoYour 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_denied403NoA person denied the held payment.Do not pay it.
review_expired403NoNobody 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_required402NoThe 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_forwarded4xx or 5xxNoThe proxy refused this request itself. It did not leave the proxy.Follow the code's entry on Errors before you re-sign.
not_enforced409NoThe 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_paymentAnyBy the earlier requestThe 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.

HeaderOnMeans
guard-statusEvery answer about a paymentWhat happened to the payment, from the list above.
guard-decision-idEvery answer with a decisionThe 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-statusEvery paid answerconfirmed 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-stepA signature that ran outfresh_quote. Request the URL again and sign the new 402.
guard-shadow-statusObserve mode onlyWhat 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.

Denied
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.

Waiting for review
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"
}
  1. Approve or deny the payment on Review.

    You see: the card moves under Resolved in the last day.

  2. Request the URL again and sign the 402 it returns.

    You see: 200 with guard-status review_approved, or 403 with review_denied.

  3. 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 answersWhen the copy arrives
409 decision_already_usedIts settlement is on file. It paid, so do not send it again.
403 payment_deniedIts payment was denied, or its review was denied or expired. The answer carries the same reasons.
ForwardedThe 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_paymentAnything 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.

warning

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.

HeaderWhat the proxy does
PAYMENT-SIGNATUREThe signed x402 v2 payment. The proxy checks it and forwards it only when the guard allows it.
X-PAYMENTAn x402 v1 payment. Refused with x402_v1.
x-vulsight-require-modeSet 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-TokenSent 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-ForNever 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/decisions does.
  • 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/context holds 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

LimitValue
Seller URL after the token2,000 characters
Request body, and each seller answer4 MiB
Seller answers relayed a UTC day, per account256 MiB
Seller answers relayed a minute, per agent120
Text sent for checking a UTC day, per account16 MiB
Text checked in one answer64 KiB
Hold on a Base payment while you reviewUp 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.