POST /preflight/x402-seller

Checks an x402 payment challenge before your agent signs it - the act an x402 agent performs more often than any other, usually towards a seller it has never dealt with:

  • screening - is the address the agent is about to credit published on a designation list (OFAC SDN and Consolidated, EU financial sanctions, UN Security Council)?
  • price coherence - does the challenge ask for the amount this resource publishes in the public x402 Bazaar census, or for more?
  • presence - is this resource in that census at all, and does it publish the address the challenge asks the agent to pay?

It returns one verdict - ok, review, stop or insufficient_coverage - with every reason attached to the block it came from, and each block’s full answer next to it. See the live /catalog for the authoritative listing and price.

The input is declarative

Read this before anything else. The agent submits what the 402 challenge served it, and the check runs on that:

  • the challenge is never replayed - we do not call the seller’s resource;
  • the seller’s origin is never contacted - no probe, no liveness ping;
  • the answer is computed offline against dated data: a snapshot of the designation lists and a daily snapshot of the public x402 Bazaar census.

So this is not a live verification of the seller. What is checked is what your agent received, against lists and a census whose dates the response gives you. That is also why it answers in milliseconds and can be called before every signature.

Scope and limits

Every response carries this sentence in data.limits, verbatim:

This is a control outcome, not a payment instruction: ‘ok’ is not a recommendation to pay, and the decision remains the caller’s. The input is declarative — the x402 challenge is never replayed and the seller’s origin is never contacted. The pay-to address is screened against the designation lists and snapshot date reported in the screening block. Price coherence and liveness hold as of the census date reported in their blocks and never as of this call: the census lists a resource only after its first settlement, so an absent resource is a coverage gap and never a signal against the seller. This check does not attest that the seller will deliver, that the resource is safe, that the price is fair, or that the origin is reachable. No score, no rating, no reputation.

In plain terms, an ok does not attest that:

  • the seller will deliver what it charges for;
  • the resource is safe to call or its content trustworthy;
  • the price is fair - only that it matches what the seller itself published;
  • the origin is reachable right now.

And there is no score: the verdict is a decision table over dated facts.

The verdict - the maximum severity of the reasons

The verdict is exactly the maximum severity among verdict_reasons, ordered insufficient_coverage < review < stop; no reason at all is an ok, and ok only. Each reason names its block, its code and its own severity, so you can recompute the verdict yourself from the reasons instead of taking ours on trust. A coverage gap next to a review reads review: the finding outranks the gap, and the gap is still listed.

BlockCodeSeverityWhen
screeningPAY_TO_SANCTIONEDstopThe pay-to address is published on a designation list
screeningPAY_TO_NOT_SCREENABLEinsufficient_coverageThe address has no shape any screened family recognizes, and no list publishes it verbatim
price_coherenceAMOUNT_ABOVE_PUBLISHEDreviewThe challenge asks for more than the resource published - the gap is in the detail
price_coherenceAMOUNT_NOT_SUBMITTEDinsufficient_coverageNo amount was sent: nothing was compared
price_coherenceRAIL_NOT_PUBLISHEDinsufficient_coverageThe resource publishes no rail for the declared network and asset
price_coherenceRAILS_AMBIGUOUSinsufficient_coverageSeveral candidate rails carry different assets and the request does not settle which
price_coherenceAMOUNT_NOT_COMPARABLEinsufficient_coverageThe published amount is not an integer of atomic units
presencePAY_TO_NOT_PUBLISHEDreviewThe resource is in the census, but publishes other pay-to addresses than this one
presenceRESOURCE_NOT_IN_SNAPSHOTinsufficient_coverageThe resource is not in the census
presenceSNAPSHOT_STALEinsufficient_coverageThe census snapshot is older than 7 days (see below)

Four rules worth stating plainly:

  • Only a designated address is stop. Nothing else in the table can produce it: not an absent resource, not an incomparable price, not a stale census. Those are coverage gaps.
  • An absent resource is never held against the seller. The census lists a resource only after its first settlement, so a new seller is absent by construction - on a market where almost everything is new. Absence is insufficient_coverage, never review and never stop.
  • Only overcharging is a reason. A challenge asking for more than the published amount is a review, with the gap stated. A challenge asking for less is reported (comparison: "below_published") and motivates nothing - a price drop between the daily census and the call is the normal case.
  • Atomic units only compare within one asset. If the candidate rails carry different assets (6 decimals here, 18 there) and the request does not say which, the answer is RAILS_AMBIGUOUS - never a “lowest amount” mixing magnitudes.

Two halves, dated separately

The two sources do not share a date, and the response never pretends they do:

  • the screening block carries the date of the designation-list snapshot;
  • the price coherence and presence blocks carry the date of the census snapshot.

Each block has its own freshness. The envelope’s provenance names both sources and carries the oldest date - a composite answer is never fresher than its oldest data.

Beyond 7 days, the census snapshot is not even consulted: price coherence and presence both come back snapshot_stale, an insufficient_coverage, rather than a comparison against data a week old. The screening comes from the other store and keeps answering - including stop.

Request body

Content-Type: application/json. Copy the challenge’s fields as they came:

FieldRequiredDescription
resourceyesThe resource URL exactly as the challenge named it - http(s), up to 2,048 characters
pay_to (alias payTo)yesThe address the challenge asks you to credit - a single on-chain address
networknoThe challenge’s network (eip155:8453, or a legacy name such as base), up to 128 characters
assetnoThe challenge’s asset (token contract or mint), up to 128 characters
amount (alias maxAmountRequired)noThe amount asked, as a decimal integer of atomic units - a JSON string or number
{
  "resource": "https://api.invoket.com/iban/screen",
  "payTo": "0x4465825B0ac713aA2Fa07Ea29023020804A128ee",
  "network": "eip155:8453",
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "maxAmountRequired": "10000"
}

The challenge’s own field names are accepted as is. payTo and maxAmountRequired are aliases of pay_to and amount: an agent copying what its challenge served should not have to rename anything to check it.

Without an amount, the price is not compared and the verdict cannot be ok (AMOUNT_NOT_SUBMITTED). Network names from both generations of the protocol are reconciled through a closed, sourced table (base is eip155:8453); a network token outside that table is never reinterpreted - it simply matches no published rail, and the answer says so.

Address forms. The pay-to address is screened exactly as /screen/address screens it, across the five families it serves (Bitcoin, EVM, Tron, Solana, Monero). An address whose form none of them recognizes - some Stellar or Algorand accounts, for instance - is not refused: the screening block says unscreenable_address_form with an empty lists_screened, and the verdict can never be ok. Refusing it would deprive those sellers of any check; calling it not_listed would be a lie.

200 response - UnifiedResponse

FieldTypeDescription
queryobjectYour fields echoed, with resource_canonical - the normalized form the census was searched with
verdictstringok, review, stop or insufficient_coverage
verdict_reasonsobject[]{ block, code, severity, detail } - one per finding; empty on ok
screeningobjectcoverage (screened / unscreenable_address_form), freshness, the unit /screen/address answer as is, and lists_screened with each list’s address count
price_coherenceobjectcoverage, freshness, declared_amount, published_amount, comparison (matches / above_published / below_published), difference, rails_matched, rails_published
presenceobjectcoverage (listed / host_only / absent / snapshot_stale), freshness, host, host_resources, last_updated, usage, pay_to (published / not_published / unknown)
limitsstringThe fixed sentence quoted above

lists_screened is what makes a not_listed opposable: it says which lists were read and how many addresses each publishes. A list publishing zero addresses is still named - the absence of a hit there is not evidence of anything.

price_coherence.coverage is one of compared, snapshot_stale, resource_not_in_snapshot, amount_not_submitted, rail_not_published, rails_ambiguous or amount_not_comparable. A fact motivates the verdict once, in the block it belongs to: an absent resource is a presence reason, and price_coherence only says, in its coverage, that it had nothing to compare.

Example - ok

POST /preflight/x402-seller with the challenge one of our own routes serves:

{
  "data": {
    "query": {
      "resource": "https://api.invoket.com/iban/screen",
      "resource_canonical": "https://api.invoket.com/iban/screen",
      "pay_to": "0x4465825B0ac713aA2Fa07Ea29023020804A128ee",
      "network": "eip155:8453",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "amount": "10000"
    },
    "verdict": "ok",
    "verdict_reasons": [],
    "screening": {
      "coverage": "screened",
      "freshness": { "source": "ofac-sdn+ofac-consolidated+eu-fsf+un-sc", "as_of": "2026-09-14" },
      "screening": {
        "query": {
          "address": "0x4465825B0ac713aA2Fa07Ea29023020804A128ee",
          "address_normalized": "0x4465825b0ac713aa2fa07ea29023020804a128ee",
          "chain_family": "evm",
          "chain_family_source": "inferred"
        },
        "verdict": "not_listed",
        "hits": []
      },
      "lists_screened": [
        { "list": "EU-FSF", "addresses": 7 },
        { "list": "OFAC-CONS", "addresses": 0 },
        { "list": "OFAC-SDN", "addresses": 1064 },
        { "list": "UN", "addresses": 0 }
      ]
    },
    "price_coherence": {
      "coverage": "compared",
      "freshness": { "source": "cdp-x402-bazaar", "as_of": "2026-10-15" },
      "declared_amount": "10000",
      "published_amount": "10000",
      "comparison": "matches",
      "difference": "0",
      "rails_matched": 1,
      "rails_published": [
        {
          "scheme": "exact",
          "network": "eip155:8453",
          "pay_to": "0x4465825B0ac713aA2Fa07Ea29023020804A128ee",
          "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
          "amount": "10000",
          "matches_query": true
        }
      ]
    },
    "presence": {
      "coverage": "listed",
      "freshness": { "source": "cdp-x402-bazaar", "as_of": "2026-10-15" },
      "host": "api.invoket.com",
      "host_resources": 13,
      "last_updated": "2026-10-14T02:20:32.587Z",
      "usage": {
        "total_calls_30d": 4,
        "unique_payers_30d": 3,
        "last_called_at": "2026-10-14T02:20:31.539Z"
      },
      "pay_to": "published"
    },
    "limits": "This is a control outcome, not a payment instruction: 'ok' is not a recommendation to pay, and the decision remains the caller's. The input is declarative — the x402 challenge is never replayed and the seller's origin is never contacted. The pay-to address is screened against the designation lists and snapshot date reported in the screening block. Price coherence and liveness hold as of the census date reported in their blocks and never as of this call: the census lists a resource only after its first settlement, so an absent resource is a coverage gap and never a signal against the seller. This check does not attest that the seller will deliver, that the resource is safe, that the price is fair, or that the origin is reachable. No score, no rating, no reputation."
  },
  "provenance": {
    "source": "ofac-sdn+ofac-consolidated+eu-fsf+un-sc+cdp-x402-bazaar",
    "fetched_at": "2026-10-15T15:45:20.615133726Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-09-14T00:00:00Z" }
  }
}

The two halves carry their own dates - designation lists as of 2026-09-14, census as of 2026-10-15 - and the envelope carries the older one. The usage figures are the census’s, served as published.

Example - review: the challenge asks for more

The same resource, with a challenge asking for 250000 atomic units (blocks unchanged except price_coherence):

{
  "data": {
    "verdict": "review",
    "verdict_reasons": [
      {
        "block": "price_coherence",
        "code": "AMOUNT_ABOVE_PUBLISHED",
        "severity": "review",
        "detail": "the challenge asks for 250000 atomic units where this resource published 10000 at the census date: 240000 more"
      }
    ],
    "price_coherence": {
      "coverage": "compared",
      "freshness": { "source": "cdp-x402-bazaar", "as_of": "2026-10-15" },
      "declared_amount": "250000",
      "published_amount": "10000",
      "comparison": "above_published",
      "difference": "240000",
      "rails_matched": 1,
      "rails_published": [ "…" ]
    },
    "…": "…"
  }
}

Twenty-five times the published price is exactly what this check exists to catch before a signature. The reverse - a challenge for 10000 on /iban/screen/batch, which publishes 15000 - is an ok with comparison: "below_published" and difference: "5000": reported, not held against anyone.

Example - stop: a designated address

A challenge that names a real resource but asks payment to an address on the OFAC SDN list - the shape of a spoofed or tampered challenge:

{
  "data": {
    "query": {
      "resource": "https://api.invoket.com/iban/screen",
      "resource_canonical": "https://api.invoket.com/iban/screen",
      "pay_to": "0x0330070FD38Ec3bB94F58FA55D40368271E9e54A",
      "network": "eip155:8453",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "amount": "10000"
    },
    "verdict": "stop",
    "verdict_reasons": [
      {
        "block": "screening",
        "code": "PAY_TO_SANCTIONED",
        "severity": "stop",
        "detail": "the pay-to address is published on a designation list (1 matching entry)"
      },
      {
        "block": "presence",
        "code": "PAY_TO_NOT_PUBLISHED",
        "severity": "review",
        "detail": "the pay-to address of this challenge is not among those this resource published at the census date"
      }
    ],
    "screening": {
      "coverage": "screened",
      "freshness": { "source": "ofac-sdn+ofac-consolidated+eu-fsf+un-sc", "as_of": "2026-09-14" },
      "screening": {
        "query": { "address": "0x0330070FD38Ec3bB94F58FA55D40368271E9e54A", "…": "…" },
        "verdict": "sanctioned",
        "hits": [
          {
            "entity_id": "OFAC-SDN:57056",
            "entity_name": "AMNOKGANG TECHNOLOGY DEVELOPMENT COMPANY",
            "list_source": "OFAC-SDN",
            "programs": ["DPRK4"],
            "listed_on": "2026-03-12",
            "chain": "ETH",
            "address_published": "0x0330070FD38Ec3bB94F58FA55D40368271E9e54A"
          }
        ]
      },
      "lists_screened": [ "…" ]
    },
    "presence": { "coverage": "listed", "pay_to": "not_published", "…": "…" },
    "…": "…"
  }
}

Two reasons, two severities, one verdict: the maximum, stop. The review from presence is still served - the resource exists and publishes a different address, which is the second thing the agent should know.

Example - insufficient_coverage: a resource never settled

A challenge for one of our routes that nobody had settled yet at the census date (screening as in the ok example):

{
  "data": {
    "query": {
      "resource": "https://api.invoket.com/company/lookup",
      "resource_canonical": "https://api.invoket.com/company/lookup",
      "pay_to": "0x4465825B0ac713aA2Fa07Ea29023020804A128ee",
      "network": "eip155:8453",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "amount": "10000"
    },
    "verdict": "insufficient_coverage",
    "verdict_reasons": [
      {
        "block": "presence",
        "code": "RESOURCE_NOT_IN_SNAPSHOT",
        "severity": "insufficient_coverage",
        "detail": "this resource is not in the census, which lists 13 other resource(s) on the same host: the census only lists a resource after its first settlement"
      }
    ],
    "price_coherence": {
      "coverage": "resource_not_in_snapshot",
      "freshness": { "source": "cdp-x402-bazaar", "as_of": "2026-10-15" },
      "declared_amount": "10000",
      "published_amount": null,
      "comparison": null,
      "difference": null,
      "rails_matched": 0,
      "rails_published": []
    },
    "presence": {
      "coverage": "host_only",
      "freshness": { "source": "cdp-x402-bazaar", "as_of": "2026-10-15" },
      "host": "api.invoket.com",
      "host_resources": 13,
      "last_updated": null,
      "usage": null,
      "pay_to": "unknown"
    },
    "…": "…"
  }
}

The address was screened (not_listed), but the price could not be compared and presence could not be confirmed - so not ok. The host is known, the resource is not yet: the census lists a resource only after its first settlement, and the answer says so instead of turning novelty into a signal. The absent resource motivates the verdict once, in presence.

Errors

Only requests the service cannot answer leave the 200 range - none of them are billed.

StatuscodeCase
400MISSING_PARAMETERresource or pay_to absent or empty
400INVALID_INPUTresource longer than 2,048 characters or not http(s) (its content is not echoed back); pay_to containing a space; network or asset longer than 128 characters; amount that is not a decimal integer of atomic units
400UNRECOGNIZED_ADDRESS_FORMATpay_to that has no address shape at all
400INVALID_BODYBody that is not valid JSON
405METHOD_NOT_ALLOWEDAny method other than POST
503DATA_UNAVAILABLEThe designation-list store or the census is not loaded: two blocks out of three at full price would be a silent degradation
500INTERNALInternal error (detail logged, not exposed)
{ "code": "INVALID_INPUT", "error": "parameter 'resource' must be the http(s) URL of the paid resource, as served by the 402 challenge" }
{ "code": "INVALID_INPUT", "error": "parameter 'amount' must be a decimal integer of atomic units, as published by the 402 challenge" }

Attribution

Designation data comes from the OFAC SDN and Consolidated lists (US Treasury, public domain), the EU financial sanctions list (European Commission) and the UN Security Council consolidated list. Census data comes from the public x402 Bazaar discovery index of the Coinbase Developer Platform (cdp-x402-bazaar), as published at the snapshot date.

See also