# SpendPreflight developer docs Operator: SpendPreflight. Scout, Verified Seller and Python remain previews; npm 0.2.0 and signed decisions are live. # Check before your agent pays A small, explicit decision between a payment quote and a signature. https://docs.spendpreflight.com/ ## Choose a starting point Use HTTP for a direct screening call, MCP for an assistant tool, or the local SDK guard in the payment path. No SpendPreflight caller account, API key or OAuth is required. An agent still needs its own wallet and payment authorization to pay. ## Three outcomes Allow: proceed only within your policy. Hold: ask a human or explicit policy authority. Block: stop. A model deciding to call a screening tool does not enforce these outcomes; put the guard before signing. ## Availability in this documentation HTTP checks, signed preflight decisions, MCP and npm 0.2.0 are live. The Index interface and rules are deployed, but the crawler is disabled pending capacity checks: missing evidence stays unknown. Scout and Verified Seller remain staging previews. Python is not published on PyPI. ## Public prices GET /v1/check | $0.01 | Wallet/name screening and domain signals POST /v1/preflight | $0.02 | Allow, hold or block plus reasons GET /v1/endpoint | $0.005 ยท requires fresh indexed data | Cached unpaid endpoint observations get_service_info / health / sample | Free | Reference and service state --- # HTTP quickstart Try a preflight without adding a wallet or a paid retry. https://docs.spendpreflight.com/http/ ## Send a quote or cart POST one challenge (x402 v1 or v2) or cart, plus rules and context.spent_today_usd. Use resource_url if the challenge omits it, and resource_method when a public index URL has multiple methods. The sample is an explicit trial: three calls per IP per UTC day, shared with MCP. An exhausted trial returns 402. It never automatically pays. ```javascript // Plain fetch: trial only, no signer and no automatic paid fallback. export async function preflightTrial(input, request = fetch) { try { const response = await request("https://api.spendpreflight.com/v1/preflight?trial=1", { method: "POST", redirect: "error", headers: { "content-type": "application/json" }, body: JSON.stringify(input), signal: AbortSignal.timeout(8000), }); if (!response.ok) return { decision: "hold", reasons: [`screening unavailable (HTTP ${response.status})`] }; const result = await response.json(); if (!["allow", "hold", "block"].includes(result?.decision) || !Array.isArray(result.reasons) || !result.reasons.every(r => typeof r === "string")) { return { decision: "hold", reasons: ["invalid screening response"] }; } return result; } catch { return { decision: "hold", reasons: ["screening unavailable"] }; } } ``` ## Paying for screening When you choose paid mode, use your x402 payment client for the SpendPreflight request. Confirm exact USDC on Base, the public payTo, and the $0.02 fee before signing. That fee buys the screening result even if the merchant decision is hold or block. It never authorizes or pays the merchant being reviewed. ## Bounded input Input hardening enforces 64 KiB actual body bytes, JSON depth 32, a 10-second body read deadline, 32 payment options and 100 entries per rule list. Text must be bounded and valid UTF-8. Invalid requests are rejected before payment. Check malformed-input tests and OpenAPI for field bounds. --- # Connect with MCP One public Streamable HTTP endpoint for assistants and agent tools. https://docs.spendpreflight.com/mcp/ ## Connection Add https://api.spendpreflight.com/mcp as a remote Streamable HTTP server in a compatible client. No caller credentials. Client setup screens differ; the JSON-RPC example below exercises the protocol directly. get_service_info is always free. check_payee and preflight_payment use the shared trial first, then x402 payment in _meta. ```javascript // JSON-RPC requests for a stateless Streamable HTTP MCP connection. // A client sends initialize, notifications/initialized, then tools/call. export const initialize = { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "spendpreflight-docs", version: "1.0" } }, }; export const initialized = { jsonrpc: "2.0", method: "notifications/initialized" }; export const serviceInfo = { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "get_service_info", arguments: {} } }; // POST to https://api.spendpreflight.com/mcp with Content-Type: application/json // and Accept: application/json, text/event-stream. get_service_info is always free. ``` ## Tools and references check_payee screens a name, domain or wallet; preflight_payment decides a challenge or cart. The check_endpoint tool reads cached public observations for $0.005. Three free resources expose default rules, pricing and trust; two prompts are preflight-before-paying and check-a-merchant. Free-trial exhaustion is an expected quota result, not a successful screen. --- # JavaScript guard Enforce a decision in the client that creates the payment authorization. https://docs.spendpreflight.com/npm/ ## Local policy first The published package is spendpreflight 0.2.0, released through GitHub Actions with provenance. The guard provides atomic budgets, network-bound assets and strict recursion protection on Node 20+. Import guard from the package, configure your normal x402 client, then pass both to this factory. No key is embedded in the sample. ```javascript // Pass guard imported from spendpreflight and your configured x402 client. // This example is local-only: it makes no screening HTTP request. export function installLocalGuard(client, guard) { return guard(client, { remote: false, rules: { maxPerPaymentUsd: 0.10, holdAboveUsd: 0.05, dailyCapUsd: 1, allowedNetworks: ["eip155:8453"], usdcOnly: true }, onHold: () => false, // route to an explicit human/policy approval in your app }); } ``` ## Budget reservations Version 0.2.0 reserves approved atomic amounts before signing, serializes concurrent decisions and retains reservations if signing fails or the payment is abandoned. The budget resets only as the UTC day advances. It is per guard/process, not a chain settlement ledger or a fleet-wide budget. ## Optional remote checks Enable remote screening explicitly. Trial uses plain fetch and never falls back to payment. Paid remote screening needs a separate locally guarded client with an exact payTo/network/asset policy and its own budget. Never reuse the merchant guard recursively. Only the selected offer is screened. Unknown/failed remote results hold by default; the legacy explicit onError:allow disables that protection. --- # Python guard Typed, local spending rules for the asynchronous Python x402 client. https://docs.spendpreflight.com/python/ ## Publication pending The Python 0.1.0 source, pytest suite and Trusted Publishing workflow are prepared. The PyPI project is not yet published; do not rely on an unrelated package claiming the name. Owner account/2FA, release and attestation checks must finish before an installation command is recommended. ## Use the existing client Attach Guard.before_payment with client.on_before_payment_creation. Local mode needs no service account or model call. Default budgets are $1 per payment, hold above $0.25, daily $25; Base USDC only. The async guard reserves budget before signing; use one guard per event loop/budget. Failed authorizations retain reservations. RemoteOptions defaults to a free-trial HTTP call; paid screening requires an explicitly separate bounded payment client. ```python """Shared pure advisory tool implementation; the payment guard still must be attached.""" from __future__ import annotations import json from typing import Any from spendpreflight import Rules, evaluate_local def check_selected_offer(requirement_json: str, resource_url: str, max_usd: str = "1") -> dict[str, Any]: """Local rule check only. No OFAC claim, network call, key or payment.""" if len(requirement_json.encode("utf-8")) > 65536: raise ValueError("Payment option must fit in 64 KiB") value = json.loads(requirement_json) if not isinstance(value, dict): raise ValueError("Provide one selected payment option as a JSON object") return evaluate_local(value, resource_url, Rules(max_per_payment_usd=max_usd)).to_dict() ``` ## LangChain and CrewAI The local wrapper in the example evaluates one chosen offer, never invokes a model or wallet, and is not an OFAC screen. The package includes real LangChain StructuredTool and CrewAI BaseTool wrappers tested through their invoke/run interfaces. An agent selecting either tool does not replace payment-path enforcement. --- # Rules reference Defaults are conservative starting points. Your caller owns the actual spend ledger. https://docs.spendpreflight.com/rules/ ## HTTP and MCP defaults HTTP rules use snake_case; the JS SDK uses camelCase. New Index settings apply only to remote SDK mode. Cart totals are already USD; no network/asset constraint is invented for card checkout. Sanctions and spending blocks outrank holds. All returned reasons should remain visible to the approver. max_per_payment_usd (default 1): Block when the selected amount exceeds this USD cap. hold_above_usd (default 0.25): Hold above this amount when the maximum has not already blocked it. daily_cap_usd (default 25): Block when context.spent_today_usd plus the quote exceeds this. HTTP callers supply their own ledger. allowed_networks (default ["eip155:8453","base"]): Allowed network identifiers. Supply the exact challenge network; default accepts both Base forms. usdc_only (default true): Require a recognized USDC contract on that network. Other tokens are not assumed to equal USD. strict_allowlist (default false): HTTP preflight holds a merchant domain outside domain_allowlist. A nonempty payto_allowlist also requires that payee. The local SDK accepts either allowlist, so configure deliberately. new_domain_days (default 30): Hold a known registration age below this many days unless the domain is allowlisted. Unknown age is reported, not invented. screen_sanctions (default true): Screen public OFAC wallet/name matches. Disabling this removes that protection. max_price_multiple (default 5): hold above this multiple of the public category median; null disables. require_live (default true): hold after three usable failed unpaid probes. Missing observations are unknown. hold_on_payto_change (default true): hold for seven-day observed payee changes or a quote differing from the latest observation. domain_allowlist (default []): Domain plus subdomains; no wildcard syntax. Treat allowlists as policy, not proof of ownership. domain_blocklist (default []): Block listed domains and subdomains. payto_allowlist (default []): Wallet identifiers. EVM addresses ignore letter case; non-EVM identifiers preserve case. payto_blocklist (default []): Block listed payees. Block takes precedence over approval. ## Missing data No domain age, missing Index evidence, or an unpriced asset is not positive evidence. Inspect data_as_of and index_evidence. The current gate does not automatically block every unknown/stale component; add stricter policy in your caller if required. A 7-day index is a record of unpaid probes, not proof an endpoint delivers after payment. --- # Every decision reason Use decision and flags for control flow; preserve the human-readable explanation. https://docs.spendpreflight.com/reasons/ ## Preflight reason templates Existing reasons are strings prefixed allow:, hold: or block:, not a universal enum. Values inside braces vary. The three endpoint_* identifiers and invalid_index_rule are prefixes followed by detail. This list covers the deployed gate; unknown future reasons must not be treated as allow. block | no payment options found in input | No usable options; correct the request. hold | could not determine merchant domain | Supply the merchant domain/resource URL. block | domain {domain} is on blocklist | Your policy blocks this merchant. hold | domain {domain} not on allowlist | Strict domain policy needs review. block | merchant name exactly matches an OFAC SDN entry | Verify the possible identity match before proceeding. hold | merchant name possibly matches an OFAC SDN entry | Ambiguous names require a human to disambiguate. hold | domain registered {days} days ago (< {threshold}) | A new domain signal is not evidence of fraud. hold | domain has no DNS A record | No A record observed. DNS errors remain unknown. block | unsupported or malformed payment option | Unsupported scheme, oversized amount or malformed EVM destination. block | invalid payment amount | Negative or nonfinite payment amount. block | network {network} not allowed | Network differs from caller policy. block | asset {asset} is not a known USDC contract | Wrong network/asset pair or another currency. hold | amount could not be priced in USD | No conversion assumption is made. block | amount ${amount} exceeds max ${maximum} | Single-payment budget exceeded. hold | amount ${amount} above hold threshold ${threshold} | Human/policy approval required. block | daily cap ${cap} would be exceeded (spent ${spent}) | Daily budget exceeded using supplied/reserved spend. block | payTo {payTo} is on blocklist | Blocked wallet. hold | payTo {payTo} not on allowlist | Configured wallet allowlist requires review. block | payTo {payTo} is an OFAC SDN-listed address | Public sanctions-list wallet match; verify before acting. hold | payment option has no payTo | Cannot identify the receiving wallet. hold | invalid_index_rule | Invalid category-price configuration. hold | endpoint_price_above_category_median | quote exceeds the configured category median multiple. hold | endpoint_failed_last_3 | three usable failed unpaid checks; not paid-delivery testing. hold | endpoint_payto_changed | payee changed for this network/asset; investigate before paying. allow | all rules passed | No configured rule fired. This is not certification or a guarantee. ## Payee check flags Risk uses the strongest applicable signal. A name match is not proof the merchant is the listed entity. Address/name data is public OFAC screening, not certification. sanctioned_address | high | Exact wallet match in the public SDN index. sanctioned_name_exact | high | Exact normalized name/alias match; identity still needs review. sanctioned_name_possible | medium | Fuzzy name match, not proof of identity. invalid_domain | medium | Domain could not be interpreted as a public merchant domain. new_domain | medium | Known domain age below 30 days. domain_age_unknown | unknown | No usable registration date; unknown is not low risk. domain_no_dns | medium | No A record observed. MX is reported separately. ## Request and payment errors 400/405/408/413 reject bad method/input/body before payment. Index lookup may return invalid_url, invalid_method, endpoint_not_listed (404), method_required (409) or index_unavailable (503). receipt_signing_unavailable and facilitator_unavailable are 503 before settlement. settlement_outcome_unknown means a transfer may already have happened: check the original authorization and chain, never automatically create a fresh payment. --- # Framework integration guides Use tools for explanations and a guard for payment authority. https://docs.spendpreflight.com/frameworks/ ## Vercel AI SDK Prepare a tool with the merchant challenge and explicit rules as input. Inject a separately bounded screeningFetch into the operator-maintained tool factory. Preserve decision, reasons and receipt. Stop execution on non-2xx/malformed responses; only the payment client may act on an allow. The tested SDK example is published in the SDK repository for release 0.2.0. ## LangChain JS Use the check_payee tool factory with wallet/name/domain inputs and the $0.01 screening fee. It screens a payee and does not authorize a payment. A standalone integration package is the current upstream contribution path; no new community-tools PR or package is claimed. ## OpenAI Agents The published SDK example uses the actual function-tool interface, rejects invalid inputs and propagates HTTP/schema failures. Import the operator-maintained factory, inject the screening transport and keep all signing outside the model tool. No upstream example PR is claimed. ## GOAT and AgentKit The GOAT plugin is prepared locally with check_payee/preflight_payment and operator disclosure; it has not been submitted. The AgentKit provider is awaiting review in coinbase/agentkit PR #1545. Neither status means a merged/released integration. Budget the separate screening fee explicitly and enforce holds/blocks before merchant payment. ## Python frameworks LangChain Python and CrewAI examples ship with the prepared Python package. They are local-only advisory wrappers with no model/network spend. Add them to your framework's tools list and wire Guard into the actual async x402 client separately. --- # How x402 payment works A quote is data. Signing is authority. Keep the decision between them. https://docs.spendpreflight.com/x402/ ## The sequence 1. Request a resource without payment. 2. Read the402 challenge. 3. Select and validate the exact scheme, network, asset, amount and payTo. 4. Run local rules and optional SpendPreflight screening. 5. Sign only on an approved allow. 6. Retry with the payment authorization. The seller verifies and settles through its facilitator, then returns the result and payment response. ## USDC on Base Our live service receives USDC on eip155:8453, asset 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0xBf4164e07552e4c1b1B1597E72C7acAA3F0a1c58. Six decimal places means 10000 atomic units=$0.01, 20000=$0.02. The cached endpoint lookup is 5000=$0.005. A familiar token address on another chain is not equivalent. ## Two different payments The screening call pays SpendPreflight for a decision. Any later merchant payment is separate, chosen and signed by your client. We never sign, settle or custody the merchant payment under review. A paid hold/block is a delivered screening result, not a rejected screening request. ## Uncertain settlement A lost settlement response may follow an actual transfer. Inspect the original nonce/transaction before retrying. Changing facilitator or signing a new authorization can double-pay. Our failure handling returns an explicit unknown outcome; it does not promise a refund or automatic fallback. --- # Verify a decision offline Signed receipts bind every returned decision field. https://docs.spendpreflight.com/receipts/ ## Trust the key first Obtain /.well-known/spendpreflight-keys.json over the trusted service origin and pin it separately. A key supplied by an untrusted receipt sender is not an identity proof. verifyReceipt (JS) and verify_receipt/verifyReceipt (Python) perform no network request. The full response, including reasons, chosen option and receipt/input hash, must match the JWS. ```javascript // Pass verifyReceipt from the published 0.2.0 SDK and a separately trusted key set. // No automatic key fetch, receipt persistence or payment occurs here. export async function checkedDecision(response, trustedKeys, verifyReceipt, options = {}) { const verified = await verifyReceipt(response, trustedKeys, options); if (!verified.valid) return "hold"; return verified.decision; // caller still enforces hold/block before payment } ``` ## Time and rotation Default maximum age is one hour with 60 seconds clock tolerance. Explicit archival mode removes only the age requirement; it does not make an old decision current. Save public keys with receipts and refresh revocations. Signature validity is our statement, not a guarantee, caller identity check or paid-settlement proof. JWS is readable, so keep receipts private. ## x402 delivery receipts The paid HTTP preflight route uses the official offer-receipt extension as well. Its signed offer commits payment terms; its separate delivery receipt follows settlement. Trial and MCP decisions have the decision-body JWS without any fabricated paid-delivery proof. No receipt history or caller request bodies are stored by this service.