A pre-sign check is worth more when it leaves a trail a third party can
check. Every keyed check_swap call persists a durable evidence record —
routescore.preflight_record.v0 — and returns three additive fields that
link the response to it: record_id, evidence_bundle_id, and
record_output_hash. Weeks later, anyone holding the record can re-derive
the hash offline and confirm the evidence is exactly what the check showed
at decision time.
This closes the fourth step of the composition Routescore participates in as one element — the evidence/attestation element alongside your planner, executor, and wallet:
plan → preflight (Routescore) → execute (venue/agent) → record (Routescore)The record is evidence of what was observed, modeled, and not evaluated at a point in time. It is not an outcome claim, not advice, and never an execution instruction.
1. The three fields check_swap now returns
Alongside the verdict, reasons, and trust envelope documented in
Add a pre-sign evidence check to your agent,
every keyed POST /api/public/v1/check/swap response carries:
| Field | What it is |
|---|---|
record_id | Id of the persisted preflight record. Retrieve the full document via GET /api/public/v1/records/{record_id} or the get_preflight_record MCP tool |
evidence_bundle_id | Id of the evidence bundle stored with the record. Minted with the response — inside it — so the id is part of the hashed evidence, not a label added afterwards |
record_output_hash | sha256:<hex> canonical-JSON content hash of the record — re-derivable offline from the retrieved document (recipe below) |
Three properties worth encoding in your agent's expectations:
- Additive and back-compatible. Existing integrations keep working; the fields are appended to the same response shape.
- Both evaluated answers are recorded. A
200(clear/caution) and a422(unsupported) are both evaluated answers, so both persist a record. Schema-invalid400s never reach evaluation and are never recorded. - Nullable for one honest reason only. If the durable write fails, the
check response still returns in full — verdict and trust envelope
unaffected — with all three fields
nullplus arecord_persistence_failedcaveat incaveats[](mirrored intrust.caveats[]). Anullrecord_idmeans "not persisted", stated plainly; it never silently degrades into a fake id.
2. What a preflight record contains
The record is a response-superset, never a fork: the full check_swap
response object nests verbatim under response — no field renamed,
flattened, or re-summarized. Around it, the record adds only:
| Record field | What it is |
|---|---|
schema | routescore.preflight_record.v0 |
record_id | Storage id (excluded from the hash) |
recorded_at | ISO-8601 UTC write time (excluded from the hash) |
request | An identity-redacted, whitelisted echo of the evaluated request (notional, chain, route, tokens, slippage allowance, declared actor type) |
actor | Identity-free actor context: actor_type is human | agent | multisig | unknown — declared by the caller, never verified. Anything else degrades to unknown, never a guess |
policy_ref | Reserved for a policyId@revision policy fingerprint; null on check_swap today |
response | The response exactly as returned to the caller (minus the two record-linkage fields record_id / record_output_hash, which cannot be embedded in the content they hash — evidence_bundle_id is inside, because it was minted with the response) |
integrity | hash_algo (sha256), output_hash, and a human-readable verification recipe |
Two boundaries are structural, not policy:
- Identity-free by construction. Your
check_swapcall can declare an optionalactor_typein the request body; it is recorded as declared. The record never stores wallet addresses, ENS names, or emails — any actor label that looks like one is dropped before recording, not masked. - Read-only evidence, never signing material. A response containing
execution or signing material (
calldata,tx,transaction,signature,signed_payload) is rejected outright rather than stored. A preflight record can never be replayed into an execution.
3. Re-verify record_output_hash offline
The hash identifies the evidence content, not the storage row:
record_id, recorded_at, and integrity are excluded, so re-recording
identical evidence yields the identical hash. It is a content hash, not a
signature — the same single recipe used by routescore.evidence_bundle.v0
and routescore.attested_artifact.v2, so there is exactly one verification
recipe across Routescore artifacts.
The recipe, as stated in every record's integrity.verification field:
- Take the retrieved
recorddocument. - Remove
record_id,recorded_at, andintegrity. - Serialize the remainder as canonical JSON — ECMAScript
JSON.stringifyapplied to the recursively key-sorted value: arrays kept in order, no whitespace, ECMAScript number rendering and string escaping, no Unicode normalization,undefineddropped. - SHA-256 the UTF-8 string and prefix
sha256:. - Compare with
integrity.output_hash— they must be equal.
The serialization is pinned to ECMAScript JSON.stringify semantics on
purpose, because "JSON" alone is not one serialization. Numbers are the
usual divergence: ECMAScript renders 0.000001 as 0.000001, while
Python's json.dumps renders it as 1e-06 — a different byte string, so a
different hash. Non-JavaScript implementations must reproduce ECMAScript
serialization semantics (numbers especially) or use the Node reference
snippet below, which is normative:
import { createHash } from 'node:crypto';
function sortDeep(value) {
if (Array.isArray(value)) return value.map(sortDeep);
if (value && typeof value === 'object') {
const out = {};
for (const key of Object.keys(value).sort()) {
if (value[key] !== undefined) out[key] = sortDeep(value[key]);
}
return out;
}
return value;
}
// `record` is the document from GET /api/public/v1/records/{record_id}
const { record_id, recorded_at, integrity, ...body } = record;
const recomputed =
'sha256:' + createHash('sha256').update(JSON.stringify(sortDeep(body))).digest('hex');
console.log(recomputed === integrity.output_hash); // must print trueAnyone can run this — an auditor, a counterparty, a future you — without a Routescore account, because the input is the record document itself. If any field of the evidence were edited after the fact, the recomputed hash would no longer match.
4. Retrieve records: REST and MCP
Both endpoints are keyed (Power tier, rs_live_… keys) and owner-scoped:
a key only ever sees records belonging to its own account. A record id from
another account is indistinguishable from a missing one — both return 404.
List your records (newest first, keyset-paginated):
curl "https://www.routescore.io/api/public/v1/records?limit=20" \
-H "Authorization: Bearer rs_live_..."Returns summaries — record_id, recorded_at, created_at, chain_id,
verdict, evidence_bundle_id, record_output_hash,
methodology_version, outcome_label_id — plus next_cursor (an opaque
cursor for the next page; null on the last page). limit accepts 1–100,
default 20.
Fetch one record in full:
curl "https://www.routescore.io/api/public/v1/records/{record_id}" \
-H "Authorization: Bearer rs_live_..."Returns { record, evidence_bundle }: the full
routescore.preflight_record.v0 document — kept pristine, so the
offline recipe above works on it directly (the trust envelope the gateway
appends lives at the body top level, outside the document) — plus the
stored routescore.evidence_bundle.v0 object (null if none was
captured).
From an agent, over MCP: the get_preflight_record tool in
@routescore/mcp takes
record_id and returns the same document, owner-scoped to the configured
key's account. Setup is the same two-minute flow as check_swap — see
API & MCP access.
5. How an agent should cite its evidence
The point of the record is that a decision can name its evidence in a form someone else can check. A minimal citation an agent writes into its own decision log:
Preflight evidence: record_id <uuid> · evidence_bundle_id <id> ·
record_output_hash sha256:<hex> · methodology_version routescore.public_api.v1 ·
verdict caution (modeled, point-in-time)Three rules make the citation worth keeping:
- Cite
record_id+record_output_hashtogether. The id retrieves the document; the hash lets a reader confirm the retrieved document is the evidence the agent claims it acted on. - Cite
evidence_bundle_idfor the bundle, not as a substitute. It names the stored evidence bundle and is itself part of the hashed response — quoting it ties the citation to content, not just storage. - Never upgrade the citation into an outcome claim. The record proves what the check showed at decision time — verdict, caveats, gap states, methodology version. What happened after is a separate question, answered by outcome labels (below), not by the record.
6. Outcome labels arrive later — never at decision time
Each record summary carries an outcome_label_id, null at creation. It
is set only after an after-the-fact outcome label reconciles what the check
showed at decision time with what was later observed on independent
settlement data. Labeling never happens at decision time, and the original
record is never edited — the before and the after both survive intact.
That separation is deliberate: it is what lets records accumulate into calibration evidence (how modeled calls compare with realized outcomes, measured in the open) instead of hindsight-adjusted stories.
7. Access, limits, and honest edges
- Records are created by keyed
check_swapcalls; the REST API and MCP server are bundled with the Power tier. Key setup: API & MCP access. - Rate limits are per key — read
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reseton every response, including the records endpoints. - If the record store is unavailable when you call
check_swap, you still get your verdict: linkage fieldsnullplus therecord_persistence_failedcaveat. The evidence never fails because the ledger hiccupped — but nothing was persisted, and the response says so. - Records are read-only. There is no update or delete surface; corrections happen as new records, never edits.
The companion guides cover the check itself (Add a pre-sign evidence check to your agent) and the evidence questions worth asking before a tokenized-asset swap (What to check before an AI agent touches a tokenized asset).
Routescore is read-only decision support: it never signs, executes, routes
funds, custodies assets, or gives investment advice. All figures are
modeled, point-in-time, and carry a methodology_version. A preflight
record is evidence of what a check showed at a moment — not a prediction,
not an outcome claim, and not a verification of safety, sellability, or
rights. No paid placement; see /neutrality.