Help center

Verifiable pre-sign evidence records

Every keyed check_swap call persists a durable, hash-verifiable evidence record — what a routescore.preflight_record.v0 contains, how to retrieve it by record_id over REST or MCP, how a third party re-derives record_output_hash offline, and how agents cite evidence_bundle_id.

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:

FieldWhat it is
record_idId 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_idId 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_hashsha256:<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 a 422 (unsupported) are both evaluated answers, so both persist a record. Schema-invalid 400s 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 null plus a record_persistence_failed caveat in caveats[] (mirrored in trust.caveats[]). A null record_id means "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 fieldWhat it is
schemaroutescore.preflight_record.v0
record_idStorage id (excluded from the hash)
recorded_atISO-8601 UTC write time (excluded from the hash)
requestAn identity-redacted, whitelisted echo of the evaluated request (notional, chain, route, tokens, slippage allowance, declared actor type)
actorIdentity-free actor context: actor_type is human | agent | multisig | unknowndeclared by the caller, never verified. Anything else degrades to unknown, never a guess
policy_refReserved for a policyId@revision policy fingerprint; null on check_swap today
responseThe 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)
integrityhash_algo (sha256), output_hash, and a human-readable verification recipe

Two boundaries are structural, not policy:

  • Identity-free by construction. Your check_swap call can declare an optional actor_type in 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:

  1. Take the retrieved record document.
  2. Remove record_id, recorded_at, and integrity.
  3. Serialize the remainder as canonical JSON — ECMAScript JSON.stringify applied to the recursively key-sorted value: arrays kept in order, no whitespace, ECMAScript number rendering and string escaping, no Unicode normalization, undefined dropped.
  4. SHA-256 the UTF-8 string and prefix sha256:.
  5. 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 true

Anyone 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:

  1. Cite record_id + record_output_hash together. The id retrieves the document; the hash lets a reader confirm the retrieved document is the evidence the agent claims it acted on.
  2. Cite evidence_bundle_id for 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.
  3. 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_swap calls; 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, and X-RateLimit-Reset on every response, including the records endpoints.
  • If the record store is unavailable when you call check_swap, you still get your verdict: linkage fields null plus the record_persistence_failed caveat. 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.