# `preflight_action` — Open Standard, v0 (DRAFT)

Schema identifier: `routescore.preflight_action.v0`
Companion JSON Schema: [`preflight_action.v0.schema.json`](./preflight_action.v0.schema.json)
Draft date: 2026-07-09 · Reference implementation: Routescore (`/api/public/v1/check/swap`, `@routescore/mcp`)

---

## 1. Status

**v0 PUBLIC DRAFT.** This document and its companion schema are published as
versioned, reviewable artifacts. The generalized `preflight_action` surface
remains a draft and is not callable unless an implementation explicitly says
otherwise. Registry submissions remain founder-gated. This draft consolidates the
vocabularies of ROU-679 (preflight interface), ROU-707 (preflight record), and
ROU-714 (open standard + policy hooks) into one specification, per ROU-714's
instruction to not create a second competing spec. See
[§12 Consolidation decisions](#12-consolidation-decisions).

What is shipped today vs what this draft defines:

| Surface | State |
|---|---|
| `POST /api/public/v1/check/swap` (`clear \| caution \| unsupported`, trust envelope, 400/422 semantics) | **Shipped.** This spec is a strict superset/generalization of it; nothing here contradicts it. |
| `POST /api/public/v1/policy/evaluate` (`allow \| warn \| block` policy decisions) | **Shipped.** Reused as the policy-evaluation component inside `preflight_action`. |
| `preflight_action` generalized request/response, flat policy grammar, record persistence | **Draft** (this document). |
| Basis-regime policy hooks (`max_basis_bps`, `max_reference_staleness_seconds`, `block_when_market_closed`) | **Reserved — reference implementation conforms (ROU-715).** The shipped policy surface (`agent_policy.v1` / `policy/evaluate`) now ACCEPTS these fields, never evaluates them, never treats them as satisfied, and surfaces them in `not_evaluated_fields` + a caveat per [§6.2](#62-reserved-field-convention). The basis monitor itself is not shipped (ROU-710 / ROU-720). |
| Self-rug-class policy hooks (`require_can_sell_observed`, `require_privilege_surface_scan`, `block_when_fill_path_curation_unknown`) | **Reserved.** Privilege-surface, can-sell, and fill-path-curation evaluation are not shipped; the probe feasibility work is a separately timeboxed spike (ROU-721, Relay disclosure 2026-07-09). See [§6.2](#62-reserved-field-convention). |
| Anonymous keyless first call (Agent-First rule 2) | **Roadmap.** The shipped gateway requires an API key. `check_swap` accepts a key from any signed-in tier (including the free agent tier); modeled quote and scenario compute remain Power-only. This spec defines object shapes, not the auth tier. |

Normative keywords MUST / MUST NOT / SHOULD / MAY follow RFC 2119.

## 2. Motivation

Agentic onchain finance is composed from elements — a planner proposes an
action, an executor (venue, wallet, or agent framework) carries it out, and
evidence elements document what was checkable before and after. This
specification defines the **preflight element**: a single call that returns,
in one object, what is known, unknown, stale, unsupported, and caveated about
a proposed onchain action — plus the caller's own declared policy evaluated
against it — before anything is signed. It is one composable element alongside
the others, never a gatekeeper above them; it reports evidence and gaps, and
the caller's own software decides whether to act.

## 3. Terminology

| Term | Meaning |
|---|---|
| **Implementation / producer** | Software that evaluates a preflight request and emits a `preflight_action` response. Routescore is the reference implementation. |
| **Agent consumer** | An AI agent (or agent framework) that calls a producer and relays results to a human. Agent consumers carry conformance duties too ([§11.2](#112-conformance-agent-consumers)). |
| **Check** | One evaluation component (chain support, route quality, modeled slippage, token registry recognition, policy rule, …). Each check can run, produce a finding, or be not evaluated. |
| **Finding** | A caution-level observation produced by a check (e.g. `token_unverified`, `slippage_exceeds_allowance`). |
| **Verdict** | The response-level summary of findings: `clear \| caution \| unsupported`. An evidence statement, never an instruction ([§7.1](#71-verdict-semantics)). |
| **`score_state`** | The gap state of the evidence itself: `valid \| partial \| stale \| degraded \| unsupported \| unavailable`. Independent axis from the verdict ([§7.2](#72-verdict--score_state-are-independent-axes)). |
| **Caveat** | A pre-written sentence describing a limit of the evidence. Flat strings (shipped) plus severity-structured entries (this spec). |
| **`relay_to_user`** | One pre-written sentence the agent consumer MUST quote verbatim to the human ([§7.4](#74-relay_to_user)). |
| **Policy** | A caller-declared constraint document evaluated against the request. Policy decisions (`allow \| warn \| block`) are the caller's own policy speaking, not the producer's verdict. |
| **Record** | The persisted, hash-verifiable form of a response plus actor/policy context ([§9](#9-record-persistence)). |
| **Evidence bundle** | The richer reviewable decision object (`routescore.evidence_bundle.v0`); a preflight response MAY link one via `evidence_bundle_id`. |
| **`methodology_version`** | Identifier of the evaluation methodology that produced a specific response. Present in every response. Orthogonal to the spec version ([§10](#10-versioning-rules)). |
| **Reserved / roadmap** | Capability-status markers for fields not shipped today ([§6.2](#62-reserved-field-convention)). |

## 4. The composition diagram

```text
plan → preflight (Routescore) → execute (venue/agent) → record (Routescore)
```

Binding diagram rules (from the composed-element doctrine,
`brand/POSITIONING_TRUST_ANALOGIES.md`):

- In every rendering of this flow, the preflight element appears **alongside**
  planner / executor / wallet elements — one box in the flow, never a layer
  drawn above or wrapping the others.
- The preflight element attests narrow, versioned, dated facts with visible
  gaps. It does not select, rank, execute, or approve the action.
- Copy derived from this spec is subject to the banned-vocabulary list in
  [§14](#14-claims-boundary-compliance-note).

## 5. Request object

`preflight_action` request. Generalizes the shipped `CheckSwapRequest`; every
shipped `check_swap` request is a valid `preflight_action` request with
`action_type` defaulting to `swap`.

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `schema` | const `routescore.preflight_action.v0` | no | — | Optional self-identification. |
| `action_type` | `swap \| lend \| borrow \| stake \| bridge \| agent_call \| tokenized_asset_exposure` | no | `swap` | Coverage varies by implementation; an unimplemented `action_type` MUST yield `verdict: unsupported`, never a guess. |
| `actor_type` | `human \| agent \| multisig \| unknown` | no | `unknown` | Declared, not verified. Producers make no identity claims. |
| `chain_id` | integer ≥ 1 | no | `1` | `4663` = Robinhood Chain (modeled support level). |
| `dest_chain_id` | integer ≥ 1 or null | no | — | For `bridge` actions. |
| `asset_in` | string | no | — | Input asset address or symbol. Canonical name; see aliases below. |
| `asset_out` | string | no | — | Output asset address or symbol. The output side is the one checked for registry recognition (shipped behavior). |
| `token_in` / `token_out` | string | no | — | **Deprecated aliases** of `asset_in` / `asset_out`, retained verbatim from shipped `check_swap`. If an alias and its canonical field are both present with different values, the producer MUST return HTTP 400 `invalid_input`. |
| `notional_usd` | number > 0, ≤ 10¹² | **yes** | — | Action size in USD. Matches shipped bounds. |
| `venue` | string | no | — | Optional venue identifier when the caller knows it independently of `route`. |
| `route` | string | no | — | Route id, e.g. `uniswap-v3-005`, `uniswap-v3-rho`. A route on the wrong chain MUST NOT be silently scored (shipped `route_chain_mismatch` behavior). |
| `slippage_allowance_bps` | number 0–10000 | no | `50` | Per-request intent; distinct from the policy constraint `max_slippage_bps`. |
| `policy_id` | string | no | — | Reference to a stored or built-in policy. Mutually exclusive with `policy`. |
| `policy` | object ([§6](#6-policy-object)) | no | — | Inline policy document. Mutually exclusive with `policy_id`. Unknown or invalid policy → HTTP 400 (`unknown_policy` / `invalid_policy`), matching the shipped gateway. |
| `evidence_context` | object (open) | no | — | Caller-supplied context for the record. MUST NOT contain wallet addresses, ENS names, emails, or any identity; producers redact identity before evaluation and logging (shipped guardrail precedent). |

Producers MAY ignore unknown top-level request fields, with two named
carve-outs where [§12.6](#126-field-name-frictions-vs-the-shipped-openapi-honest-list)
wins over leniency — these are known-rejected spellings, not unknown fields:

- Bare `notional` MUST be rejected with HTTP 400 `invalid_input`
  (underspecified: no unit). The companion schema forbids the property name
  outright (`"notional": false`).
- A deprecated alias (`token_in` / `token_out`) present alongside its
  canonical field (`asset_in` / `asset_out`) with a **different value** MUST
  be rejected with HTTP 400 `invalid_input`. This rule is enforced by
  producer behavior and conformance tests, not by the companion schema
  ([§11.1](#111-conformance-implementations-producers), [§12.6](#126-field-name-frictions-vs-the-shipped-openapi-honest-list)).

Inside `policy`, unknown fields MUST always be rejected
([§6.3](#63-strict-policy-parsing)).

## 6. Policy object

Schema identifier: `routescore.preflight_policy.v0`. A flat, snake_case
constraint grammar designed for agent builders to author by hand. It maps
onto the shipped `routescore.agent_policy.v1` grammar for evaluation
([§12.4](#124-policy-grammar-consolidation)).

### 6.1 Fields

| Field | Type | Status | Semantics |
|---|---|---|---|
| `policy_version` | const `routescore.preflight_policy.v0` | active | Optional self-identification. |
| `policy_id` | string | active | Stable operator-chosen identifier (fingerprinted into `policy_result.policy_fingerprint`). |
| `label` | string | active | Human label. |
| `revision` | integer | active | Monotonic revision counter. |
| `allowed_chains` | integer[] | active | Chain-id allowlist. Non-empty list ⇒ closed-by-default: any chain not listed is a violation. |
| `allowed_assets` | string[] | active | Asset allowlist (addresses preferred over symbols). Closed-by-default when non-empty. |
| `max_notional_usd` | number > 0 | active | Violation when `notional_usd` exceeds it. |
| `max_slippage_bps` | number 0–10000 | active | Violation when modeled slippage exceeds it. If slippage was not modeled, the rule is **not evaluated** — surfaced, never assumed satisfied. |
| `token_recognition_required` | boolean | active | When true: `token_safety.state` other than `recognized` is a violation. Registry recognition only — recognition asserts presence in a registry, nothing more. |
| `source_freshness_floor` | number > 0 (seconds) | active | Maximum tolerated source age in seconds (mirrors shipped `freshnessFloor.maxAgeSeconds`). Violation when any consulted source is older, or freshness is `unknown`/`unavailable`. |
| `approval_threshold_usd` | number ≥ 0 | active | At or above this notional, the response MUST set `required_human_approval: true`. |
| `max_basis_bps` | number > 0 | **reserved** | Tokenized-asset vs reference-price basis ceiling. Basis monitor not shipped (ROU-710/715/720). |
| `max_reference_staleness_seconds` | number > 0 | **reserved** | Reference-price staleness ceiling. (Renamed from ROU-714's `max_reference_staleness` for unit explicitness — see §12.3.) |
| `block_when_market_closed` | boolean | **reserved** | Off-hours regime rule for tokenized assets tracking market-hours references. |
| `require_can_sell_observed` | boolean | **reserved** | When it activates: a response-side `can_sell` state other than `observed_at_T` is a violation. `can_sell` is point-in-time only and never forward-looking; its defeat-ability caveat travels in-band always — the self-removing token class observed on Robinhood Chain (Relay Protocol disclosure, 2026-07-09) can pass a quote-time sell probe and still remove balances afterwards (ROU-721). |
| `require_privilege_surface_scan` | boolean | **reserved** | When it activates: a response-side `token_privilege_surface` state of `not_evaluated` is a violation. Static privilege-surface observation (mint authority, blacklist/whitelist, trading pause, transfer taxes, upgradeable proxy, owner privileges) is the pre-sign check the self-removing class cannot defeat; probe feasibility is spike-gated (ROU-721). |
| `block_when_fill_path_curation_unknown` | boolean | **reserved** | When it activates: an unknown fill-path curation state (a venue-operated, mutable, non-machine-readable block/verify list inside the route's fill path) is a violation. The resulting decision remains advisory; the producer never blocks anything itself (ROU-721). |

### 6.2 Reserved-field convention

`status: reserved` pins a field's **name and type now** so early adopters do
not invent competing names, while stating plainly that **no conforming
implementation evaluates it yet**. Reserved-field behavior is normative:

- A producer MUST accept a reserved field without error.
- A producer MUST NOT evaluate a reserved field, and MUST NOT treat it as
  satisfied.
- A producer MUST list every reserved field it received in
  `policy_result.not_evaluated_fields` and add a `structured_caveats` entry
  (severity ≥ `notice`) saying the rule was not evaluated.
- When the underlying capability ships, the field moves `reserved → active`
  in a v0.x additive revision, with a changelog entry.

Honest schema limitation: the surfacing rule is **conformance-test-enforced,
not schema-enforced**. The reserved field arrives in the *request* document
while the surfacing happens in the *response* document, and the companion
JSON Schema validates each document in isolation — it cannot express
cross-document conditionality. The schema does define
`policy_result.not_evaluated_fields` (array of strings) so conforming
producers have a pinned place to put the surfaced names; producer contract
tests ([§11.1](#111-conformance-implementations-producers) rule 5) assert
the request→response coupling.

`status: roadmap` marks capabilities named in prose only (not yet in the
schema): the tokenized-asset passport reference and the observed
token-state fields below. Roadmap fields enter the schema as `reserved`
before activation.

**Response-side `reference_price` + `market_regime` (ROU-715, 2026-07-10):**
promoted from roadmap prose to schema fields. The reference implementation
ships them on every evaluated `check_swap` response as always-present
gap-state evidence objects — explicit `state`
(`unavailable` when a tokenized-asset reference relationship is recognized
but unobserved; `unsupported` when none is recognized), `regime: unknown`,
null observation timestamps, and in-band caveats. Observation *values*
(freshness readings, staleness seconds, basis, open/closed regimes) remain
gated on the basis monitor (ROU-710/720) and are never guessed. The
tokenized-asset passport carries the same two fields when it ships.

**Observed token-state roadmap fields (re-specced 2026-07-09, ROU-721).**
The former single "observed can-sell state" roadmap item is split and
extended after the Relay Protocol disclosure of a self-removing scam-token
class observed in the wild on Robinhood Chain (2026-07-09). All of these are
observed on-chain states, never intent labels; none may ever emit a state
that reads as "safe":

- `can_sell` — `observed_at_T | not_evaluated`. Explicitly point-in-time,
  never forward-looking, and NEVER a headline "sellable" state: the observed
  self-removing class passes a quote-time sell probe and removes balances
  afterwards, so the defeat-ability caveat travels in-band with every
  `observed_at_T` value. The calibration ledger MUST NOT ingest naive
  sell-simulation results as outcome labels.
- `token_privilege_surface` — static bytecode/config observations of
  deployer-retained privileges: mint authority, blacklist/whitelist, trading
  pause, transfer taxes, upgradeable proxy, owner balance privileges. States
  are `observed | not_evaluated` per indicator; there is deliberately no
  `none` state that could read as an all-clear — absence of findings stays
  caveated to the methodology version. This is the pre-sign, read-only check
  the self-removing class cannot defeat.
- Liquidity state — LP lock state, depth, single-wallet LP concentration, as
  observed facts.
- Holder concentration and deployer-funding links — published as facts
  ("N of top-M holders funded by the deployer within K blocks (observed)"),
  never as intent or coordination attributions.
- Token lifecycle facts — token age, verified-source state, deployer history,
  as on-chain observations.
- `fill_path_curation` — `{ venue, state: curated_mutable, machine_readable,
  observed_at }` for routes whose fill path transits a venue operating a
  mutable block/verify list (e.g. Relay on Robinhood Chain), with the caveat
  that exit liquidity via that path is subject to venue curation decisions
  independent of the token's own contract. If such a list becomes queryable,
  `blocked_by_fill_path_venue: observed` MAY be added as an evidence input.

### 6.3 Strict policy parsing

The policy object is **closed**: unknown fields inside `policy` MUST be
rejected with HTTP 400 `invalid_policy`. Rationale: a typo'd constraint
silently ignored is a constraint the caller believes is enforced. This is the
conservative-default doctrine applied to parsing.

## 7. Response object

One call, whole answer (Agent-First rule 1): a single response carries the
verdict, policy result, violations, caveats, trust envelope, and evidence
record id. No multi-call ceremony.

The response = the shipped `CheckSwapResponse` core **plus** additive
preflight fields. Every shipped `check_swap` response body is a valid
(subset) instance of this object — the JSON Schema deliberately keeps the
additive fields optional for exactly this reason ([§12.5](#125-schema-requiredness-compromise)).

**Core fields (shipped; required):**

| Field | Type | Notes |
|---|---|---|
| `verdict` | `clear \| caution \| unsupported` | See §7.1. |
| `chain` | object or null | `{ chainId, name, supportLevel: live \| modeled \| recognized }` (camelCase inherited verbatim from the shipped contract — §12.6). |
| `route` | object or null | Shipped route view: `id`, `name`, `protocol`, `chainId`, `grade`, `qualityScore`, `modeledRouteLeakBps`, `publicMempoolMevBps` (nullable), `modeledSlippageBps`, `expectedTotalLossUsd`, `orderFlow`, `confidenceLabel`. All modeled figures are modeled, point-in-time. |
| `token_safety` | object | `{ state: recognized \| unverified \| not_evaluated, recognized, flags[], caveats[] }`. Registry recognition only — never sellability, rights, redemption, custody, liquidity, or investment-quality verification. There is no positive-assurance flag. |
| `reasons` | string[] | Stable machine reason codes (`token_unverified`, `route_chain_mismatch`, `sequencer_ordering_uncalibrated`, …). Open vocabulary within v0.x. |
| Trust envelope (flattened + nested `trust`) | — | `score_state`, `source_freshness`, `methodology_version`, `confidence_band`, `caveats[]`, `commercial_disclosure`, and the full nested `trust` object with `generated_at` and `decision_support_only: true`. Identical to the shipped `TrustEnvelopeFlattened`. Present on success AND error bodies. |

**Additive preflight fields (this spec; REQUIRED from producers implementing
the `preflight_action` surface, optional in the schema for subset
compatibility):**

| Field | Type | Notes |
|---|---|---|
| `schema` | const `routescore.preflight_action.v0` | Response self-identification. |
| `action_type` | enum (§5) | Echo of the evaluated action type. |
| `actor_type` | enum (§5) | Echo of the declared actor type. |
| `policy_result` | object or null | Null when no policy was supplied. Otherwise: `decision` (`allow \| warn \| block`), `policy_fingerprint` (identity-free `policyId@revision`), `grammar_version`, `reasons[]`, `applied_safe_default` (verbatim shipped field name — §14), `not_evaluated_fields[]` (reserved or data-starved rules), `refused` / `refusal_reason` (shipped guardrail semantics). |
| `policy_violations` | array | Violation entries `{ code, message, decision?, rule? }`, most severe first. Empty array when a policy ran clean; MAY be omitted only when no policy was supplied. |
| `structured_caveats` | array | `{ severity: info \| notice \| warning, text, code? }`. Severity-bearing counterpart of the flat `caveats[]` (§12.7). Every `warning` entry MUST also appear as a flat string in `caveats[]` so envelope-only consumers still see it. |
| `relay_to_user` | string | §7.4. |
| `required_human_approval` | boolean or null | §7.5. |
| `evidence_bundle_id` | string or null | Id of the persisted evidence record/bundle. Null on keyless/anonymous calls — persistence is the key-gated part (Agent-First rule 2), never the verdict. |

### 7.1 Verdict semantics

| Verdict | Means | Does NOT mean |
|---|---|---|
| `clear` | Every check that ran completed without a caution-level finding, within the stated coverage, freshness, and `score_state`. | **`clear` is never a recommendation.** It is not an endorsement of the action, asset, venue, or route; it is not a statement that the action will succeed, that the token can be sold, or that unmodeled exposures are absent. It reports the absence of findings among the checks listed — nothing else. |
| `caution` | At least one check produced a finding, or an input was unknown/unverified and the producer defaulted toward caution rather than guessing. Unknown token → `caution`. Policy `warn`/`block` ⇒ at least `caution` (§12.1). | Not a prohibition, and not a prediction of loss. The caller's own software decides. |
| `unsupported` | The action, chain, asset, or required data is outside coverage: the producer **cannot evaluate** and says so instead of guessing. Missing data → `unsupported`. HTTP 422 on HTTP surfaces (§8). | Not an error, and not a negative finding about the action itself. It is a first-class coverage answer. |

Derivation precedence: `unsupported` (cannot evaluate) → otherwise `caution`
(any finding or defaulted unknown) → otherwise `clear`. Producers MUST NOT
emit any verdict value outside this closed enum, and MUST NOT invent a
danger/blocked verdict without observable evidence (shipped rule: no
fabricated negative verdicts without a live data feed to back them).

### 7.2 Verdict × `score_state` are independent axes

The verdict summarizes **findings**; `score_state` summarizes **evidence
completeness/quality** (`valid | partial | stale | degraded | unsupported |
unavailable` — the gap-state vocabulary, policy-usable and versioned per
`GAP_STATE_DOCTRINE.md`). They are deliberately independent:

- `clear` + `partial` is legal and shipped today (e.g. token registry not
  evaluated on a chain: no finding, but incomplete evidence). Consumers MUST
  read both axes; `clear` with a non-`valid` `score_state` is *not* a full
  pass.
- `verdict: unsupported` implies `score_state: unsupported` (shipped
  coupling).
- Gap states are never silently omitted; absence of data is itself a state.

### 7.3 Caveats

The flat `caveats[]` (trust envelope, shipped) always remains the canonical
verbatim-sentence channel. `structured_caveats[]` adds machine-usable
severity so agent consumers can enforce relay rules ([§11.2](#112-conformance-agent-consumers)).
At least one caveat is always present (decision-support boundary — same rule
as the evidence bundle); the companion schema enforces the floor with
`minItems: 1` on both the flat `caveats[]` and `trust.caveats[]`.

### 7.4 `relay_to_user`

A single pre-written sentence, authored by the producer, that the agent
consumer MUST quote **verbatim** in its final user-facing answer
(Agent-First rule 3: the producer authors the sentence the end user reads;
structured fields the model cannot paraphrase away). Producers MUST derive
it from the verdict, the highest-severity caveat, and the score_state; it
MUST itself pass the claims-boundary scan (§14).

### 7.5 `required_human_approval`

Advisory, boolean-or-null:

- `true` when `approval_threshold_usd` was met or exceeded, or when the
  policy's approval rule could not be evaluated (conservative default: an
  unevaluated approval rule reads as approval required).
- `false` only when a policy ran and no approval rule tripped.
- `null` / absent when no policy was supplied.

The producer cannot enforce approval; the caller's own software does.
Emitting `true` is evidence that the caller's declared policy asks for a
human, nothing more.

## 8. Error semantics

Matches the shipped gateway exactly. The critical convention — inherited
from shipped `check_swap`, and **different** from the quote endpoints — is
that HTTP 422 carries a **full response body**, not an error envelope:

| HTTP | Body | When | `error.code` values |
|---|---|---|---|
| 200 | `preflight_action` response, verdict `clear` or `caution` | Request evaluated. | — |
| 400 | Error envelope (`{ error: { code, message } }` + trust envelope flattened) | Malformed before evaluation: missing/non-positive `notional_usd`, invalid `chain_id` or `slippage_allowance_bps`, malformed JSON, alias/canonical mismatch (§5), unknown or invalid policy. | `invalid_input`, `invalid_json`, `unknown_policy`, `invalid_policy` |
| 401 / 403 | Error envelope | Keyed surfaces: missing/unknown key; tier below the gated tier. | `unauthorized`, `forbidden` |
| 422 | **Full `preflight_action` response with `verdict: unsupported`** — an answer, not an error | Evaluated to unsupported (unsupported chain, out-of-coverage action_type, required data unavailable). | — (no `error` object) |
| 429 | Error envelope + `Retry-After` | Rate limited. | `rate_limited` |
| 500 | Error envelope | Unexpected producer failure. | `internal_error` |

Every body — success and error — carries the trust envelope (shipped rule).
Consumers MUST branch on the presence of `error` / `verdict`, not on the
status code alone.

## 9. Record persistence

Schema identifier: `routescore.preflight_record.v0` (consolidating ROU-707's
working title `preflight_record.v1` — §12.2). A record is the **persisted
form of a preflight response**: response-superset by construction, never a
fork. The full response object is embedded **verbatim** under `response`;
no response field is renamed, flattened, or duplicated at the record level.

| Field | Type | Required | Notes |
|---|---|---|---|
| `schema` | const `routescore.preflight_record.v0` | yes | |
| `record_id` | string | yes | Stable record identifier. **Excluded from the content hash** (alongside `recorded_at`) so the hash identifies the evidence content, not the storage row: re-recording identical evidence yields the identical hash. |
| `recorded_at` | ISO-8601 UTC | yes | **Excluded from the content hash.** |
| `request` | request object (§5), sanitized | no | Identity-redacted echo of the evaluated request. |
| `actor` | `{ actor_type, label? }` | yes | Identity-free: declared actor type plus an optional caller label. MUST NOT contain wallet addresses, ENS names, emails, or biometric/identity data (shipped guardrail + no-identity-claims rule). |
| `policy_ref` | `{ policy_fingerprint, grammar_version? }` or null | no | Identity-free `policyId@revision` fingerprint of the policy that ran. |
| `response` | response object (§7), verbatim | yes | The preflight response as returned to the caller. |
| `integrity` | `{ hash_algo: "sha256", output_hash, verification }` | yes | Hash recipe identical to `routescore.evidence_bundle.v0` / `routescore.attested_artifact.v2`: remove `record_id`, `recorded_at`, and `integrity`, sort object keys recursively (arrays keep their order), serialize with ECMAScript `JSON.stringify`, SHA-256 the UTF-8 bytes, prefix `sha256:`. (`record_id` joins `recorded_at` in the exclusion set — a consolidation of ROU-707's build contract: the hash names the evidence content, not the storage row.) **Canonical JSON is pinned to ECMAScript `JSON.stringify` semantics** — no whitespace, ECMAScript number rendering and string escaping, no Unicode normalization applied. Other languages render some numbers differently (Python `json.dumps` writes `1e-06` where `JSON.stringify` writes `0.000001`), which changes the bytes and the hash; non-JavaScript verifiers MUST reproduce ECMAScript serialization semantics or use the normative reference verifier (`canonicalize()` / `contentHash()` in the reference implementation's `src/lib/attestation.ts`). |

Records are read-only evidence: no calldata, no transaction payloads, no
signing material — signing outputs and signing keys alike — ever. The
companion schema enforces the canonical
property names only at the response **top level** (`false` property
schemas — JSON Schema cannot cheaply express a recursive key ban);
producers MUST additionally reject execution-material keys **at any
depth** of the stored response, matching keys after normalization
(lowercase, `_`/`-` stripped) against the deny set {`calldata`, `tx`,
`transaction`, `signature`, `signed_payload`, `raw_transaction`,
`transaction_payload`, `unsigned_tx`, `signing_payload`, `private_key`,
`signing_key`, `secret_key`, `mnemonic`, `seed_phrase`, `keystore`} — so
nested (`route.calldata`, `wallet.private_key`), array-borne
(`quotes[0].tx`), and case/separator
variants (`callData`) are rejected too. The set covers signing OUTPUTS
(calldata, payloads, signatures) and KEY-SHAPED fields (private keys,
mnemonics, keystores); bare `secret` / `seed` / `key` are deliberately
excluded (false-positive risk on benign fields). This deep-scan rule is
producer/conformance-test-enforced, not schema-enforced (same convention
as §6.2 and §11.1 rule 11). Persistence (save/export) is the key-gated
capability; the first call is not (Agent-First rule 2 — roadmap for the
anonymous tier, see §1). `response.evidence_bundle_id`, when present, links
the record to a full `routescore.evidence_bundle.v0` object; `record_id`
identifies this record itself.

## 10. Versioning rules

Two version identifiers, deliberately orthogonal:

| Identifier | Names | Changes when |
|---|---|---|
| **Spec version** (`routescore.preflight_action.v0`, schema `$id`) | The contract: field names, types, enums, requiredness, error semantics. | Editorially, via drafts within v0.x; breaking change ⇒ `v1`. |
| **`methodology_version`** (trust envelope, every response) | The evaluation methodology that produced *this* response's numbers and verdict. | Whenever scoring/evaluation logic changes, independent of the contract. |

Within v0.x, changes are **additive-only**:

- New fields MUST be optional (or enter as `reserved`).
- The verdict enum, `score_state` enum, `actor_type` enum, and error-code
  meanings are **frozen** — agent consumers hard-code against them.
- `reasons` codes, caveat codes, and `policy_violations.rule` values are open
  vocabularies (new codes MAY appear; consumers MUST tolerate unknown codes).
- `reserved → active` transitions are additive and changelog-noted.
- Removals, renames, type changes, requiredness changes ⇒ `v1`.

The record schema version tracks the spec version. Consumers MUST tolerate
unknown fields in responses and records (forward compatibility).

## 11. Conformance requirements

### 11.1 Conformance: implementations (producers)

A conforming producer:

1. MUST answer in **one call**: verdict + policy result + violations +
   caveats + trust envelope + record id in a single response object. No
   multi-call ceremony (Agent-First rule 1).
2. MUST attach the full trust envelope (flattened + nested `trust`) to every
   body, success and error, including `methodology_version` in every
   response.
3. MUST apply conservative defaults: unknown/unverified token → `caution`;
   missing data or out-of-coverage input → `unsupported`; MUST NOT guess,
   fabricate a value, or emit an unbacked negative verdict.
4. MUST be deterministic: identical sanitized input + identical
   `methodology_version` ⇒ identical verdict and ordered reason codes
   (shipped policy-evaluation precedent).
5. MUST handle reserved policy fields per §6.2 (accepted, not evaluated,
   surfaced in `not_evaluated_fields` + caveat), and MUST reject unknown
   policy fields per §6.3.
6. MUST emit `relay_to_user` and `structured_caveats` on the
   `preflight_action` surface, with every `warning` caveat mirrored into the
   flat `caveats[]`.
7. MUST NOT execute, sign, route funds, take custody, or emit calldata /
   transaction payloads / signing material in any field.
8. MUST keep records and evaluation identity-free (redact wallet addresses,
   ENS names, emails before evaluation and logging).
9. MUST return HTTP 422 with a full response body for `verdict: unsupported`
   on HTTP surfaces, and 400 error envelopes for pre-evaluation input
   failures (§8).
10. MUST pass all generated copy fields (`caveats`, `structured_caveats`,
    `relay_to_user`, `policy_result` messages) through the claims-boundary
    vocabulary scan (§14).
11. MUST return HTTP 400 `invalid_input` when a deprecated alias
    (`token_in` / `token_out`) and its canonical field (`asset_in` /
    `asset_out`) are both present with different values (§5, §12.6).

Rules 3, 4, 6, 10, and 11 are contract-testable and belong alongside the
claims harness (ROU-717 pattern). Two rules are enforceable **only** by
conformance tests and producer behavior, never by the companion JSON Schema:
rule 11 (alias/canonical value mismatch — JSON Schema cannot compare the
values of two fields) and the reserved-field surfacing half of rule 5
(request→response cross-document coupling, §6.2).

### 11.2 Conformance: agent consumers

Agent-consumer conformance (skill, MCP client, agent framework) splits into
two classes by what can actually be proven from outside the agent.

#### 11.2.1 Relay-output conformance (MUST — mechanically testable)

These duties are observable in the agent's final user-facing text, so they
are hard requirements. A conforming agent consumer:

1. MUST quote `relay_to_user` **verbatim** in its final user-facing answer —
   not paraphrased, softened, or dropped.
2. MUST surface every `warning`-severity caveat to the user, and SHOULD
   surface `notice`-severity caveats.
3. MUST NOT present `clear` as a recommendation, endorsement, or approval to
   proceed.
4. MUST read `score_state` and disclose `partial`, `stale`, and `degraded`
   states to the user rather than presenting the result as complete.
5. MUST represent `unsupported` as "not evaluated" — never as a pass and
   never as a failure of the action itself.

Relay-output conformance is mechanically testable via the **agent-relay
eval harness** (ROU-714): automated tests run real agent sessions (Claude
Code, Cursor, custom) with the skill/MCP and assert that the final
user-facing text contains `relay_to_user` verbatim and every
`warning`-severity caveat, and that verdict and score-state survive into
the answer. Consumer integrations SHOULD run the harness in CI; the
reference implementation gates its own skill releases on it.

#### 11.2.2 Execution-control conformance (SHOULD — not mechanically provable)

An agent consumer SHOULD treat the verdict and any policy decision as
advisory evidence, not as execution authorization, letting its own software
(and a human, when `required_human_approval` is true) decide whether to
act.

This is a SHOULD, stated honestly: **Routescore cannot verify an agent's
internal authorization logic from the outside** — no external harness can
prove what an agent's decision code did with the verdict, only what the
agent's observable relay behavior shows (§11.2.1). Producers and the
reference implementation therefore make no conformance claims about
consumer execution control; the duty is stated so integrators design for
it, not because it can be certified.

## 12. Consolidation decisions

Three prior effort vocabularies merge here. The mappings, and the honest
frictions, are recorded rather than papered over.

### 12.1 Verdict vocabulary: ROU-679's PASS/WARN/BLOCK → shipped enum

**Decision: the normative verdict enum is `clear | caution | unsupported`** —
already shipped in `check_swap`, already agent-consumed. ROU-679's
PASS/WARN/BLOCK is retired as a verdict vocabulary. Rationale is the claims
boundary: **"BLOCK" implies execution control the producer does not have.**
A read-only evidence element cannot block anything; emitting a verb that
implies it could misrepresents the product's authority and invites consumers
to treat evidence as enforcement.

Non-normative alias table (for readers arriving from ROU-679):

| ROU-679 term | This spec | Note |
|---|---|---|
| PASS | `clear` | With the §7.1 proviso: never a recommendation. |
| WARN | `caution` | |
| BLOCK | *(none)* | Closest composite: `verdict: caution` + `policy_result.decision: block` + entries in `policy_violations`. The **policy layer** keeps `allow \| warn \| block` (shipped `policy/evaluate` — unchanged): there it is the *caller's own declared policy* speaking about the caller's own rules, a usage that stays inside the claims boundary. It is never the producer's verdict. Normative coupling: `policy_result.decision` of `warn` or `block` ⇒ verdict MUST NOT be `clear` (schema-encoded via `if`/`then` in the companion schema). |

ROU-679's required output fields all land in this spec: evidence bundle id
(`evidence_bundle_id`), policy result (`policy_result`), caveats
(`caveats` / `structured_caveats`), unsupported components (`reasons` +
`not_evaluated_fields` + gap states), source freshness summary
(`source_freshness`), export hash (`integrity.output_hash` on the record).
The dependency summary is composable via the shipped dependency-graph
endpoint and is **roadmap** as an inline response field.

### 12.2 Record: ROU-707's `preflight_record.v1`

**Decision: the record is the persisted form of a preflight response —
response-superset, zero forked fields** (§9). The full response nests
verbatim under `response`; the record adds only `record_id`, `recorded_at`,
`actor`, `policy_ref`, an optional sanitized `request` echo, and `integrity`
(content hash, evidence-bundle recipe). ROU-707's working title
`preflight_record.v1` is **renumbered to `preflight_record.v0`** so the
record schema version tracks this spec's version; "v1" would have implied a
stability the draft does not yet have.

### 12.3 Policy hooks: ROU-714 / founder steer

All ROU-714 policy hooks are adopted (§6.1). The three basis-regime hooks
(`max_basis_bps`, `max_reference_staleness_seconds`,
`block_when_market_closed`) enter as **`status: reserved`** because the basis
monitor is not shipped (ROU-710/715/720) — the spec pins their names without
overclaiming shipped capability. One deliberate rename: ROU-714's
`max_reference_staleness` becomes `max_reference_staleness_seconds` for unit
explicitness, consistent with `source_freshness_floor` being defined in
seconds and with the shipped `maxAgeSeconds`.

### 12.4 Policy grammar consolidation

The shipped policy grammar (`routescore.agent_policy.v1`) is camelCase and
nested; the founder-steer hooks are snake_case and flat. **Decision: the
preflight policy is a new flat snake_case grammar
(`routescore.preflight_policy.v0`) with a defined mapping into the shipped
grammar for evaluation** — producers MAY translate and delegate to the
shipped engine:

| `preflight_policy.v0` | `agent_policy.v1` | Note |
|---|---|---|
| `allowed_chains` (int[]) | `chains.approved` (string[]) | Stringify chain ids; shipped `PolicyContext.chainId` is a string while `check_swap.chain_id` is an integer — adapters must convert (known friction). |
| `allowed_assets` | *(none)* | Net-new; no shipped equivalent. |
| `max_notional_usd` | `routeSizeCapUsd.maxUsd` | |
| `max_slippage_bps` | *(none)* | Net-new as a policy rule (request-level `slippage_allowance_bps` is intent, not policy). |
| `token_recognition_required` | *(none)* | Net-new. |
| `source_freshness_floor` | `freshnessFloor.maxAgeSeconds` | Same unit (seconds). |
| `approval_threshold_usd` | `manualReviewTriggers[kind=large_route].threshold` | Closest shipped analogue. |
| basis-regime hooks | *(none)* | Reserved; no shipped equivalent. |

### 12.5 Schema-requiredness compromise

The JSON Schema keeps the additive response fields (§7) **optional** so that
every shipped `check_swap` response body validates as a `preflight_action`
response subset — the compatibility requirement wins over schema-level
enforcement. Producer-side requiredness of those fields on the
`preflight_action` surface is stated normatively in prose (§11.1 rule 6) and
enforced by contract tests, not by the schema. This is a deliberate,
documented compromise, not an oversight.

### 12.6 Field-name frictions vs the shipped OpenAPI (honest list)

| Friction | Resolution |
|---|---|
| Memo says `asset_in`/`asset_out`; shipped `check_swap` says `token_in`/`token_out`. | Canonical: `asset_in`/`asset_out` (generalizes beyond token swaps). `token_in`/`token_out` retained as deprecated aliases; both-present-mismatch ⇒ 400. Honest limitation: the companion JSON Schema cannot compare the values of two fields, so the mismatch rule is enforced by producer 400 behavior + conformance tests (§11.1 rule 11), **not** by schema validation. |
| Memo says `notional`; shipped says `notional_usd`. | Shipped wins: `notional_usd`. Bare `notional` is rejected as underspecified (no unit); the companion schema forbids the property name outright (`"notional": false`) and producers MUST 400 it (§5). |
| Shipped response mixes cases: snake_case top level (`token_safety`, `score_state`) around camelCase nested objects (`chain.chainId`, `route.qualityScore`, `supportLevel`). | Inherited **verbatim** — renaming would fork the shipped contract. All *new* fields are snake_case. Case unification is deferred to v1. |
| Shipped `caveats` is `string[]`; Agent-First rule 3 wants severity. | Both: flat `caveats[]` kept canonical, `structured_caveats[]` added additively (§7.3). |
| `PolicyContext.chainId` (string) vs `chain_id` (integer). | Preflight uses integer `chain_id`; adapters stringify when delegating to `policy/evaluate`. |
| `SourceFreshnessState` (`fresh, partial, stale, unsupported, unavailable, unknown`) vs gap-state list (`valid, partial, stale, degraded, unsupported, unavailable`). | Not a conflict: two different axes. The gap-state list is exactly the shipped `ScoreState` enum; the freshness enum stays as shipped. No merging. |
| Quote endpoints use 422 as an *error envelope* (`invalid_input`); `check_swap` uses 422 as a *full response with `verdict: unsupported`*. | Preflight adopts the `check_swap` convention (§8); the divergence across shipped endpoint families is noted so integrators are not surprised. |

## 13. Out of scope

This specification does not define, and the object MUST NOT be extended to
include:

- **Execution**: signing, routing, submitting, or scheduling transactions;
  calldata or transaction payloads in any field.
- **Custody**: holding keys, funds, or assets in any form.
- **Advice**: investment, legal, securities, or tax advice; asset-merit
  opinions; "what to buy".
- **Guarantees**: outcome assurances of any kind. No field asserts that an
  action will succeed, that a token can be sold, or that a loss will not
  occur.
- **Venue rankings**: ordered venue league tables or endorsements. Route
  comparisons are per-request modeled evidence with a methodology version,
  not a ranking product; no pay-to-rank, ever.
- **Identity claims**: verification of who or what the actor is; biometric
  data.

## 14. Claims-boundary compliance note

This public draft is subject to the guardrails scan:

- Banned in normative prose and in every producer-generated copy field
  (`caveats`, `structured_caveats[].text`, `relay_to_user`, policy messages):
  **"safe"**, **"guaranteed"**, **"protected"**, **"best"** (unscoped),
  **"ultimate"**, **"the trust layer"**, **"prevents loss"**, **"insures"**,
  **"we recommend"** (list per `brand/POSITIONING_TRUST_ANALOGIES.md`;
  process per `CLAIMS_BOUNDARY_QA.md` v1.3 (current at draft time)).
- Additionally banned for the observed token-state domain (ROU-721,
  observed-states-never-intent-labels): detection branding such as
  **"rug-pull detector"**, **"scam detection"**, **"honeypot detector"**, and
  intent-attribution verbs (**"wash trading/volume"**, **"coordinated
  wallets/clusters"**, **"manipulated"**) in product copy. Producer copy
  describes observable states and cites dated sources; the facts accuse, the
  producer only observes. SEC/FBI-style fraud statistics belong in
  educational pages only, never marketing copy.
- Machine identifiers quoted verbatim from shipped contracts
  (`applied_safe_default`, `safeDefault`, `token_safety`) are field names,
  not claims copy; they are retained unrenamed to avoid forking the shipped
  surface and are always set in code formatting.
- All diagrams follow §4: the preflight element sits alongside the other
  elements of the agentic stack, one box in the flow.
- Confidence language points at published calibration and methodology
  versions, never at adjectives. `clear` is never a recommendation (§7.1).

---

## Alignment block

```text
Aligned assertions:
- assert-routescore-decision-support-not-execution (verdict is evidence, not
  control; BLOCK rejected as a verdict; no calldata anywhere)
- assert-routescore-trust-first-transparency (gap states first-class; reserved
  fields disclosed as not evaluated; schema-enforced caveat relay)
- assert-routescore-decision-record-flywheel (record = hash-verifiable
  response-superset, export-ready)
- assert-routescore-agentic-defi-trust-envelope (trust envelope on every body,
  composed-element doctrine in §4)
Potential contradiction:
- None identified; the spec is a strict superset of the shipped check_swap
  contract and defers unshipped capability behind reserved/roadmap markers.
Evidence captured:
- Shipped contract read from openapi/routescore.v1.yaml and
  app/src/lib/check-swap.ts (incl. clear+partial coexistence and
  422-with-full-body semantics).
Invalidation trigger to watch:
- ROU-714 eval-harness results showing agents strip relay_to_user/caveats
  despite the schema (would force a stronger relay mechanism in v0.x).
```
