Published 2026-09-01. Binding specification, frozen after publication.
preflight_v3 supersedes preflight_v2 at a version boundary announced 2026-09-01, after a second fixture audit by Maha Strategies. The v1 and v2 schemas and fixtures remain published for the audit trail.
This document is the binding specification for the composite preflight envelope, schema version preflight_v3. It is frozen after publication. Changes happen only at an announced version boundary with a new schema_version.
The endpoint that returns these envelopes is not built yet. This is the schema and a set of locked, really signed fixtures, published so an integrator can build against a fixed target before anything is paid. When the endpoint ships, the price is 0.015 dollars per call.
v3, 2026-09-01: closed the two refusal branches (400 invalid subject, 503 signing unavailable) with additionalProperties false and explicitly declared fields, after Maha's second fixture audit found injected keys accepted there, contradicting the every object rule. No other change.
v2, 2026-09-01: tightened after Maha Strategies' independent fixture review, before any consumer integrated v1. Changes: per component verdict enums, additionalProperties false, non empty limitations, 503 refusal shape, manifest binding to a dedicated preflight service entry authorizing the composite signer.
What each change means in practice:
preflight, which is now a service entry in the proof manifest that authorizes 0x57fF0F084Cba33e6761503f90eEF0Da9F159350c as the composite signer for both the components and the envelope. Each component's manifest also carries an informational source_service naming the underlying service that produced it.One call returns three independent checks of a single subject address, each as its own signed sub result: payability, sanctions, and trust. Nothing collapses into a single score. The envelope carries a components_evaluated count and that is the only top level tally.
The three checks run sequentially within one request, in a fixed order: payability, then sanctions, then trust. All three always run. The envelope records the observation mode and the spread in milliseconds between the first and last observation, so the small time gap is visible rather than hidden.
schema_version: the constant "preflight_v3".request: request_id, request_time in RFC3339, and subject with address, chain in CAIP-2 form, and role which is one of payer, payee, escrow_destination.components: exactly three component objects, in the order payability, sanctions, trust.observation: mode is the constant "sequential-within-one-request", and spread_ms is the integer millisecond spread across the three observations.components_evaluated: an integer from 0 to 3, the count of components whose verdict is not not_evaluated. There is no aggregate verdict and no score at the top level.envelope: signer and signature.All three components share one structure: component, verdict, reason_codes (a non empty array; not_evaluated always carries at least one), observed_at, freshness_bound_seconds, limitations (non empty, always present), subject_echo, signer (the constant composite signer), manifest with url, entry the constant "preflight", and an optional source_service, and component_digest and component_signature so each component verifies on its own, offline.
Verdict vocabulary and freshness bound, per component, locked in v2:
The list is locked at this version. Where a source service already emits a code, we reuse its literal rather than invent a new one. New codes require a version boundary.
Reused from the source services. payability, from the address preflight transfer simulation and option verdicts: simulated_ok, simulated_revert, EVM_NO_ATA_CHECK_NEEDED, ATA_MISSING_NEVER_EXISTED, ATA_CLOSED_AFTER_USE, NOT_SPL_SETTLEMENT. trust, from the reputation scorer status: insufficient_data.
Derived, because those services expose no reason code taxonomy of their own. sanctions: ofac_sdn_match, ofac_sdn_no_match. trust: erc8004_scored.
Parameterized codes attach a value after a colon: revert_reason:<reason string>, matched_label:<OFAC chain label>.
Operational, shared across all components and the refusal shapes: oracle_unreachable, rpc_timeout, invalid_subject, signing_unavailable.
The envelope is always complete and signed. Every component is present and signed even when its verdict is not_evaluated. There is never a partially signed envelope.
Total refusal happens in exactly two cases, and only before any component runs:
In every other case the caller gets a full signed envelope, with not_evaluated components where a check could not complete.
Signer: 0x57fF0F084Cba33e6761503f90eEF0Da9F159350c, published in the proof manifest as the signer of the preflight service entry, authorized for both the components and the envelope. Signing is Ethereum EIP-191 personal_sign over canonical JSON, the same discipline as our sanctions screen: json.dumps with sort_keys true and separators of a comma and a colon, which escapes non ASCII as backslash u sequences.
To verify a single component offline:
To verify the whole envelope:
JSON Schema validation is necessary but not sufficient. A document can be schema valid and still be wrong or tampered. A consumer MUST also perform these checks in code, because the schema cannot express them:
0x57fF0F084Cba33e6761503f90eEF0Da9F159350c before trusting any verdict.If any of these fail, treat the envelope as untrusted, the same as a signature mismatch.
Every verdict is point in time evidence at its observed_at, bounded by its freshness_bound_seconds. This is not a guarantee of future settlement. A payable destination can stop being payable, an OFAC snapshot can change, and reputation data can move. Read the freshness bound before relying on a verdict.
This is the clean-pass fixture, our own payee address, all three checks really run and really signed. Digests and signatures are truncated here; the published fixture carries them in full.
{
"schema_version": "preflight_v3",
"request": {
"request_id": "pf_...",
"request_time": "2026-09-01T...Z",
"subject": { "address": "0xc87a06DEE4c0E85912296002617120BBfd5EF990", "chain": "eip155:8453", "role": "payee" }
},
"components": [
{ "component": "payability", "verdict": "PAYABLE", "reason_codes": ["simulated_ok"], "observed_at": "2026-09-01T...Z", "freshness_bound_seconds": 300, "limitations": ["..."], "subject_echo": {"address":"0xc87a06DEE4c0E85912296002617120BBfd5EF990","chain":"eip155:8453","role":"payee"}, "signer": "0x57fF0F...350c", "manifest": {"url":"https://x402.nsgoods.org/proof/index.json","entry":"preflight","source_service":"payable-address"}, "component_digest": "...", "component_signature": "0x..." },
{ "component": "sanctions", "verdict": "CLEAR", "reason_codes": ["ofac_sdn_no_match"], "freshness_bound_seconds": 86400, "manifest": {"entry":"preflight","source_service":"sanctions"}, "...": "..." },
{ "component": "trust", "verdict": "NO_DATA", "reason_codes": ["insufficient_data"], "freshness_bound_seconds": 3600, "manifest": {"entry":"preflight","source_service":"trust"}, "...": "..." }
],
"observation": { "mode": "sequential-within-one-request", "spread_ms": 65 },
"components_evaluated": 3,
"envelope": { "signer": "0x57fF0F...350c", "signature": "0x..." }
}
clean-pass is our payee address: payable, clear, no trust data. sanctions-match is a live OFAC SDN address: an OFAC match, and its transfer simulation also reverts. partial-not-evaluated shows a real not_evaluated trust component; the live trust oracle returns NO_DATA for an address with no ERC-8004 registration, so that one component is marked synthetic_condition true with a one line note, because a genuine oracle outage could not be forced honestly from outside. invalid-subject is the real HTTP 400 refusal body, no envelope. signing-unavailable is the HTTP 503 refusal body, no envelope.
The paid /payable and /screen endpoints are unchanged by this document. This composite lands there only at an announced version boundary. Signed report manifest at /proof/index.json.