Preflight composite schema: preflight_v1

Published 2026-08-31. Binding specification, frozen after publication.

Superseded by preflight_v2 at a version boundary announced 2026-09-01. v1 fixtures remain for the audit trail.

This document is the binding specification for the composite preflight envelope, schema version preflight_v1. 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.

What it is

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.

The envelope

A component

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 (always present), subject_echo, signer, manifest with url and entry, and component_digest and component_signature so each component verifies on its own, offline.

Verdict vocabulary and freshness bound, per component:

Reason codes, locked

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: oracle_unreachable, rpc_timeout, invalid_subject.

Fail closed behavior

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.

Signatures and offline verification

Signer: 0x57fF0F084Cba33e6761503f90eEF0Da9F159350c, published in the proof manifest. 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:

  1. Take the component object. Remove component_digest and component_signature.
  2. Canonicalize the remainder: keys sorted, compact separators.
  3. sha256 of that string must equal component_digest.
  4. Recover the signer from the EIP-191 signature over that same string. It must equal the component signer.

To verify the whole envelope:

  1. Take the envelope. Remove envelope.signature only. Keep envelope.signer.
  2. Canonicalize the whole body.
  3. Recover the signer from the EIP-191 signature over that string. It must equal envelope.signer.

A component verifies even if you discard the rest of the envelope. Store the full body plus a manifest snapshot for long term audit, and verification keeps working after a signer rotation, because the manifest keeps a rotated signer alongside the new one with a validity boundary.

Point in time, not a guarantee

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.

Example envelope

This is the clean-pass fixture, our own payee address, all three checks really run and really signed. The full fixtures are published byte for byte with real signatures.

{
 "schema_version": "preflight_v1",
 "request": {
  "request_id": "pf_6527a6b060353100",
  "request_time": "2026-08-31T16:30:00Z",
  "subject": { "address": "0xc87a06DEE4c0E85912296002617120BBfd5EF990", "chain": "eip155:8453", "role": "payee" }
 },
 "components": [
  { "component": "payability", "verdict": "PAYABLE", "reason_codes": ["simulated_ok"], "observed_at": "2026-08-31T16:30:00Z", "freshness_bound_seconds": 300, "limitations": ["..."], "signer": "0x57fF0F...350c", "manifest": {"url":"/proof/index.json","entry":"services[7] payable-address"}, "component_digest": "...", "component_signature": "0x..." },
  { "component": "sanctions", "verdict": "CLEAR", "reason_codes": ["ofac_sdn_no_match"], "freshness_bound_seconds": 86400, "...": "..." },
  { "component": "trust", "verdict": "NO_DATA", "reason_codes": ["insufficient_data"], "freshness_bound_seconds": 3600, "...": "..." }
 ],
 "observation": { "mode": "sequential-within-one-request", "spread_ms": 12 },
 "components_evaluated": 3,
 "envelope": { "signer": "0x57fF0F...350c", "signature": "0x..." }
}

Fixtures and schema

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.

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.