Open standard · specification
Release Record v1
The full specification, as published. Verify a record at /standard, or offline with the code below.
# Release Record v1
Status: published, 29 Oct 2026. Schema: https://swapeazi.io/schemas/release-record-v1.json
Verifier: `POST https://swapeazi.io/api/v1/releases/verify` (no key). Page: https://swapeazi.io/standard
This document: https://swapeazi.io/standard/spec
SwapEazi manages cross-border payments and waits for a person's approval; every payment it manages
ends in a Release Record, the evidence of that decision, written at the moment the payment was released. It is the thing an auditor samples. It proves four things, and anyone can verify
all four without an account and without trusting SwapEazi's servers:
1. **What was decided.** The amount and currency, the organisation's own payment reference (never an
account number), the policy and policy version it was held to, whether an independent view of the
route existed and was valid at release, and the outcome (`met`, `miss`, `exception`, `not_material`).
2. **Who released it, and when.** `released_by` (a person, a system or an agent) and `released_at`,
with `recorded_at` the moment the record was written.
3. **That it has not been changed since.** `rer_hash` is a SHA-256 over everything the record states.
Change one character of a hashed field and the hash no longer matches.
4. **Where it sits in the organisation's history.** Each record carries the hash of the one before it
(`prev_rer_hash`), so no record can be removed, reordered or inserted without breaking the chain.
A signature with SwapEazi's published key proves SwapEazi issued it.
A record never blocks a payment. A release with no independent view is still recorded, as a policy miss:
the point is to record the gap, not to hide it. SwapEazi never holds or moves money; the record is
evidence of a decision the organisation made and executed through its own bank, provider or wallet.
The standard describes the record's structure and how to verify it. It does not describe how any figure
referred to by a record was produced.
## Fields
Every field below is covered by `rer_hash` unless marked "not hashed".
| Field | Type | Meaning |
|---|---|---|
| `$schema` | string | `https://swapeazi.io/schemas/release-record-v1.json`. Not hashed: records issued before the standard was published carry none and are read as v1. |
| `rer_id` | string | `rer_` followed by lowercase letters and digits. |
| `seq` | integer | Position in the organisation's chain, from 1. |
| `org_id` | string | The organisation the chain belongs to. |
| `policy_id`, `policy_version` | string, integer | The release policy in force at release. |
| `payment_ref` | string | The organisation's own reference, at most 64 characters. Anything shaped like an account number is refused at release. |
| `uetr` | string, optional | The SWIFT UETR (ISO 20022 end-to-end id) of the payment, when given. Ties the record to the payment itself. |
| `amount` | `{ value, currency }` | `value` is a decimal string with two places. |
| `amount_eur` | number or null | The amount in euro, for the materiality threshold, when convertible. |
| `intent_hash` | `sha256:` hex or null | The hash of the payment requirement the route view was made for. |
| `map_id`, `map_age_at_release_minutes`, `measure_id` | string/number or null | The independent view in hand at release, and how old it was. Null when there was none. |
| `released_at`, `recorded_at` | ISO 8601 | When it was released, and when the record was written. |
| `released_by` | `{ type, id }` | `type` is `human`, `system` or `agent`. |
| `policy_result` | object | `material`, `material_basis`, `map_required`, `map_present_and_valid`, `map_check` (found, within_expiry, within_max_age, intent_hash_matches), `within_max_all_in`, `exception` (reason and the person who signed it off, or null), `autonomy` (present only on an agent release under tiered limits), `outcome`, `misses`. |
| `reconcile_due_by` | ISO 8601 or null | When what landed must be reconciled; null when not material. |
| `reconcile` | object | **Not hashed.** Status as of the moment the record was read (`pending`, `done`, `overdue`, `not_required`). It changes after release, so it is kept beside the chain, never inside it. |
| `prev_rer_hash` | `sha256:` hex | `rer_hash` of the record before it, or the genesis hash for `seq` 1. |
| `rer_hash` | `sha256:` hex | **Not hashed** (it is the hash). |
| `signature` | string or null | **Not hashed.** An Ed25519 attestation, described below. Null only where signing was not configured. |
A field not listed here is not part of v1. The schema sets `additionalProperties: false`, so a record
with an extra field fails the schema test.
## How the hash works
1. Take the record and remove `rer_hash`, `signature`, `reconcile` and `$schema`.
2. Write it as canonical JSON: object keys sorted (by UTF-16 code unit, the default JavaScript sort) at
every level, no whitespace, strings and numbers as `JSON.stringify` writes them, arrays in order.
3. `rer_hash` is `sha256:` followed by the lowercase hex SHA-256 of that text, as UTF-8.
## How the chain works
- The genesis hash for an organisation is `sha256:` + hex SHA-256 of `swapeazi.rer.genesis:<org_id>`.
- Record 1 has `prev_rer_hash` equal to the genesis hash. Record *n* has `prev_rer_hash` equal to the
`rer_hash` of record *n-1*, the same `org_id`, and `seq` = *n*.
- Records are written to numbered slots with a write-if-new, so two releases at the same moment cannot
take the same slot and the chain cannot fork.
- A cut-off tail cannot be seen from the links alone. The organisation's own
`GET /api/v1/releases/verify-chain` (with its key) also verifies that every indexed record is present.
## How the signature works
`signature` is `base64url(JSON payload) "." base64url(Ed25519 signature over the first part)`.
The payload is `{ v: 1, typ: "release.record", id, iat, kid, rer_id, org_id, seq, rer_hash, prev_rer_hash }`.
`kid` is the first 16 hex characters of the SHA-256 of the public key (SPKI DER), so a verifier picks the
right key after a rotation. The public keys are published as a JWK set at
https://swapeazi.io/api/v1/attestations/keys (retired keys stay listed so older records still verify).
A signature is valid when it verifies against a published key, `typ` is `release.record`, and its
`rer_id`, `org_id`, `seq`, `rer_hash` and `prev_rer_hash` equal the record's. Since the signature covers
`rer_hash`, and `rer_hash` covers every hashed field, the signature covers the whole record as released.
## Verifying a record
**Online, no key:** `POST /api/v1/releases/verify` with `{ "record": { ... } }`, or
`{ "records": [ ... ] }` for a chain, or `{ "record": { ... }, "chain": [ ... ] }`. The answer names
every test (in the `checks` field): `schema`, `hash`, `chain_link`, `signature`, each `pass`, `fail`, `not_checked` (the link of
the first record of a segment that starts after `seq` 1) or `not_signed`. `valid` is true when no test
failed; `broken_at` names the first record that failed. Nothing sent is stored. AI agents can call the
MCP tool `verify_release_record` with the same body.
**Offline, with Node 18 or later and no packages:**
```js
import { createHash, createPublicKey, verify } from "node:crypto";
const canonical = (v) => Array.isArray(v) ? `[${v.map(canonical).join(",")}]`
: v && typeof v === "object" ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`
: JSON.stringify(v ?? null);
const sha = (s) => `sha256:${createHash("sha256").update(s).digest("hex")}`;
export function verifyRecord(record, before, jwks) {
const { rer_hash, signature, reconcile, $schema, ...hashed } = record;
const hash = sha(canonical(hashed)) === rer_hash;
const link = before ? record.prev_rer_hash === before.rer_hash && record.seq === before.seq + 1
: record.seq === 1 ? record.prev_rer_hash === sha(`swapeazi.rer.genesis:${record.org_id}`) : null;
let sig = null;
if (signature) {
const [head, s] = signature.split(".");
const p = JSON.parse(Buffer.from(head, "base64url").toString("utf8"));
const jwk = jwks.keys.find((k) => k.kid === p.kid);
sig = !!jwk && p.typ === "release.record"
&& verify(null, Buffer.from(head), createPublicKey({ key: jwk, format: "jwk" }), Buffer.from(s, "base64url"))
&& ["rer_id", "org_id", "seq", "rer_hash", "prev_rer_hash"].every((k) => p[k] === record[k]);
}
return { hash, link, sig };
}
```
Fetch the JWK set once and keep it with the audit file; verification then needs no network.
## Rate Receipt v1
The figures a route assessment rests on each carry a Rate Receipt (`schema: "swapeazi.rate_receipt.v1"`),
published at https://swapeazi.io/schemas/rate-receipt-v1.json. A receipt says what was read and when: the
publisher (masked as Route A, Route B on a free answer), the kind of source, `read_at`, the size priced,
the rate and the official rate, confidence, and `max_age_seconds`, the time it may be treated as current.
A missing figure is null with a reason; a missing cost is never zero. It never says how a figure was
produced.
## Versioning
- The version is in the schema URL (`release-record-v1.json`). A record names it in `$schema`.
- v1 is fixed. A new field, a removed field, or a change to what the hash covers is a new version with
a new URL; v1 records keep verifying under v1 for as long as they are kept.
- The verifier rejects a record that names a schema version it does not know, rather than guessing.
- Clarifications to this document that change no field or rule do not change the version.