Resolution Attestation Profile v1 (RAP-1)
A machine-checkable way for an off-chain resolver to show a venue that it followed its own declared resolution procedure.
| Profile ID | RAP-1 |
| Taxonomy version | 1.1.0 |
| Status | Published specification. No venue has adopted it. No regulator has reviewed it. |
| Reference implementation | security.process-attestation — BlindOracle SKU, $0.25/call |
| Endpoint | POST https://api.craigmbrown.com/v1/services/security.process-attestation |
| Identity binding | ERC-8004 passport (optional — see §8) |
| Published | 2026-09-05 (v1.1.0, same day) |
| Author | Craig M. Brown / BlindOracle |
| License | This document may be freely implemented. |
What this proves, and what it does not. RAP-1 proves that a submitted evidence log is internally consistent with a declared procedure. It does not prove the log is a true record of what happened — a fabricated but internally consistent log scores identically to a genuine one. Under the symmetrichmac-sha256scheme it also does not prove who produced the evidence; useed25519(§7.3) when attribution matters. Every deliverable carries these limits as machine-readable fields (evidence_basis,limitation,signature_binding), not merely as prose.
1. The problem
An event contract settles against an outcome. Somebody has to determine that outcome. At a CFTC-regulated venue that determination is a human process against a pre-specified authoritative source — Kalshi, for example, resolves against official government statistics, league results and regulatory announcements, with an Outcome Review Committee for ambiguous cases. On decentralized venues it is an optimistic oracle with a dispute window.
Either way, the venue carries the settlement-integrity risk and the resolver carries none of it. The resolver says "I followed the procedure." The venue has no artifact that would let it, or its auditor, or its regulator, check that claim later.
This became a live regulatory question on 2026-06-10, when the CFTC issued a Notice of Proposed Rulemaking amending Rule 40.11. Among the grounds on which an event contract may be deemed contrary to the public interest, the proposal names settlement integrity deficits — specifically a lack of clear and objective resolution criteria. Comments closed 2026-07-27.
At the same time the supply of regulated venues expanded: Payward closed its $550M acquisition of Bitnomial on 2026-05-04, giving Kraken a Designated Contract Market, a Derivatives Clearing Organization and a Futures Commission Merchant in one transaction, with perpetual futures brought onshore through a 2026-05-29 filing.
"Clear and objective resolution criteria" is a property of a declared procedure. Whether the procedure was followed on a given resolution is a separate, checkable question. RAP-1 is a format for asking the second question mechanically.
What this is not: it is not a resolution oracle, not a data source, not a dispute mechanism, and not a substitute for a venue's own review process. It sits beside them and produces one artifact: a re-computable verdict on whether a submitted evidence log is internally consistent with a declared procedure.
2. Model
Two documents in, one verdict out.
declared_process ──┐
(what you said ├──► A1 … A7 ──► verdict ∈ {conformant,
you would do) │ indeterminate,
run_evidence ──┘ non_conformant}
(what you recorded
yourself doing)
The verifier never reads the resolver's systems and never reads its own operator's ledgers. The data direction is resolver → verifier. This is deliberate: a verifier that attested to its own runs would have no third-party value.
Verification is recomputation, not judgment. There is no language model anywhere in the path. The predicate grammar (§5) is closed and non-Turing-complete, evaluated under a node budget, with no dynamic eval of any kind. Two parties running the same inputs get the same verdict, and either can reproduce the other's.
2.1 Ternary verdicts
Every check returns one of four values, and the distinction between the last two is the load-bearing design decision in this profile:
| Check verdict | Meaning |
|---|---|
pass | the evidence affirmatively supports the claim |
fail | the evidence contradicts the claim |
unverifiable | the evidence does not speak to the claim |
not_applicable | the declaration did not make this kind of claim at all |
unverifiable is never scored as fail. A resolver that omits timestamps has not been caught doing anything wrong; it has produced a log that cannot answer a timeline question. Collapsing those two states is how an attestation format becomes a false-accusation machine, and an attestation may end up in front of the resolver's own auditor or counsel.
2.2 Aggregation
any check fail → non_conformant
else any unverifiable → indeterminate
else → conformant
not_applicable checks are excluded from aggregation. So is a check the submitter opted out of entirely — currently only A7 with zero signed records. Never attempting a stronger guarantee is a normal minimal submission and must not drag an otherwise-clean result down; partially attempting one is ambiguous and is scored indeterminate. Zero evidence records never scores conformant by default.
indeterminate is the expected verdict for a good-faith first submission. conformant requires the resolver to have instrumented for it in advance.
3. Input: declared_process
{
"ordered": true, // optional; enables A2
"window": { // optional; enables the A4 window test
"start": "2026-09-05T00:00:00Z",
"end": "2026-09-05T23:59:59Z"
},
"forbidden": ["manual_override"], // optional; enables A5
"required": [ // ≤ 200 entries, objects only
{
"id": "fetch_authoritative_source",
"predicate": { /* §5 */ } // optional; enables A3 for this step
},
{ "id": "compare_to_contract_terms" },
{ "id": "publish_resolution" }
]
}
required steps must be objects with an id. The natural shorthand ["fetch", "publish"] is rejected at the boundary rather than silently promoted to {"id": "fetch"} — a coerced step carries no predicate, and would then score as a pass the submitter never actually declared.
4. Input: run_evidence
An ordered list of ≤ 4,000 record objects. Every field is optional; each one you supply unlocks a check.
{
"step_id": "fetch_authoritative_source", // A1, A2, A3, A5
"ts": "2026-09-05T14:02:11Z", // A4 (ISO-8601)
"fired": true, // A3 (or "outcome"; must be boolean)
"predicate_inputs": { // A3
"source_status": 200,
"source_id": "bls.gov/cpi"
},
"prev_sha256": "…", // A6
"signature": "…", // A7
"sig_scheme": "hmac-sha256", // A7
"pubkey": "…" // A7
}
Where a step appears more than once, checks use its first occurrence.
5. Predicate grammar
A closed expression language over predicate_inputs. Four node types; anything else evaluates to unknown.
{ "cmp": { "field": "source_status", "op": "eq", "value": 200 } }
{ "all": [ <node>, … ] }
{ "any": [ <node>, … ] }
{ "not": <node> }
Operators: eq ne lt lte gt gte in contains.
Evaluation is Kleene three-valued: all is false if any child is false, unknown if any child is unknown, else true; any is true if any child is true, unknown if any child is unknown, else false. A missing field, an unknown operator, an empty all/any list, or a TypeError on comparison all yield unknown — never a silent false.
Bounds: 2,000 nodes, depth 25. Exceeding either yields unknown.
6. The seven checks
| ID | Name | fail when | unverifiable when | not_applicable when |
|---|---|---|---|---|
| A1 | required steps present | a declared step has no evidence record | — | no required steps declared |
| A2 | declared order honored | first-seen order of ≥2 declared steps contradicts declared order | — | ordered not set, or <2 orderable steps present |
| A3 | predicate recomputation | a recomputed predicate contradicts the record's own fired/outcome | inputs missing, claim non-boolean, or predicate → unknown | no step declares a predicate |
| A4 | timeline consistency | timestamps non-monotonic, or outside a declared window | some timestamps unparseable | fewer than 2 timestamped records |
| A5 | forbidden steps absent | a declared-forbidden step_id appears | — | no forbidden steps declared |
| A6 | evidence chain linkage | a prev_sha256 does not match the recomputed hash | no record asserts prev_sha256, or only some do | exactly one record (nothing to chain) |
| A7 | signature verification | a signature fails to verify | an unsupported sig_scheme, an unavailable verifier, or no signed records (opt-out) | — |
A7 carries a binding field on pass, mirrored at the top level as signature_binding: attributable (ed25519), tamper_evident_only (hmac-sha256 or a mix), or unsigned. Two submissions can both score A7 pass and mean very different things, so never read the verdict without the binding.
A3 is the substantive one. A1/A2/A5 check that the shape of the run matches the shape of the declaration. A3 recomputes the resolver's decision logic from the inputs it recorded and compares the result to what it claims it decided. A predicate_mismatch means the resolver's own recorded inputs do not produce the outcome it published — the closest thing in this profile to catching a wrong resolution.
A3 is also the check most likely to be unverifiable in practice, because it requires the resolver to have recorded predicate_inputs at the time, in the format the declared predicate references.
7. The recipe
This section is the artifact that was missing until this document. Without it, a third party could verify an attestation but could not independently produce evidence that would earn a conformant verdict, which made conformant a state only the implementer could reach.
7.1 Canonical JSON
Deterministic serialization, used for both hashing and signing:
- object keys sorted lexicographically
- no whitespace — no spaces after
:or, undefined/ absent keys dropped- every non-ASCII character escaped as
\uXXXX(astral characters as their
UTF-16 surrogate pair, e.g. 😀 → 😀)
⚠️ The ASCII-escaping rule is load-bearing and easy to get wrong. JavaScript'sJSON.stringifydoes not escape non-ASCII; Python'sjson.dumpsdoes, by default. A producer that emits"café"where the verifier hashes"café"computes a different digest, and A6 and A7 both fail on any record containing a single accented character or emoji. Version 1.0.0 of this document shipped a TypeScript reference with exactly that defect; it is corrected below.
Reference (TypeScript):
const NON_ASCII = new RegExp("[\\u0080-\\uFFFF]", "g");
const asciiEscape = (s: string): string =>
s.replace(NON_ASCII, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
const canonicalJson = (value: unknown): string => {
if (value === null || typeof value !== "object") return asciiEscape(JSON.stringify(value));
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
const obj = value as Record<string, unknown>;
const keys = Object.keys(obj).filter((k) => obj[k] !== undefined).sort();
return `{${keys.map((k) => `${asciiEscape(JSON.stringify(k))}:${canonicalJson(obj[k])}`).join(",")}}`;
};
Python equivalent: json.dumps(obj, sort_keys=True, separators=(",", ":")) — the default ensure_ascii=True is the behaviour above; do not pass ensure_ascii=False.
Both implementations above were run against the same inputs — including accented characters and an astral emoji — and produce byte-identical output.
7.2 Chain linkage (A6)
For each record after the first:
prev_sha256 = sha256( canonical( record[i-1] minus {prev_sha256, signature, sig_scheme, pubkey} ) )
The four excluded keys are removed before canonicalization. Excluding them is what makes the chain computable in a single forward pass: a record's hash cannot depend on its own signature.
7.3 Signature (A7) — two schemes, two different guarantees
Both sign the same payload:
payload = canonical( record minus {signature, sig_scheme, pubkey} )
**ed25519 — attributable. Use this when a counterparty must be able to say who produced the evidence.**
signature = ed25519_sign( private_key, payload ) -> 64 bytes, hex-encoded (128 chars)
pubkey = raw ed25519 public key -> 32 bytes, hex-encoded (64 chars)
sig_scheme = "ed25519"
The public key cannot sign. A7 pass therefore binds the evidence to the holder of the private key, and the verifier reports binding: "attributable".
hmac-sha256 — tamper-evident only.
signature = HMAC-SHA256( key = pubkey_string_as_utf8_bytes, message = payload )
sig_scheme = "hmac-sha256"
Note the key is the pubkey string itself, UTF-8 encoded — not hex-decoded bytes. Implementers must match this byte-for-byte or A7 will fail.
Because the verification key is the signing key and travels inside the record, anyone who can check the signature can also forge it. A7 pass under this scheme means the bundle has not been altered since assembly; it attributes nothing, and the verifier reports binding: "tamper_evident_only". A submission mixing both schemes is reported at the weaker level.
Where records are produced inside a trusted execution environment, derive the key per run so the underlying secret never leaves the enclave:
pubkey = sha256_hex( `${signing_key}:${run_id}` ) # hmac-sha256 only
An unknown or unsupported sig_scheme — and an ed25519 verifier that is unavailable on the machine running the check — yields unverifiable, never fail. An absent verifier is a fact about that machine, not about the evidence.
7.4 Complete producer
import { sha256 } from "@noble/hashes/sha256.js";
import { hmac } from "@noble/hashes/hmac.js";
import { ed25519 } from "@noble/curves/ed25519.js";
import { bytesToHex } from "@noble/hashes/utils.js";
const CHAIN_EXCLUDE = ["prev_sha256", "signature", "sig_scheme", "pubkey"];
const SIG_EXCLUDE = ["signature", "sig_scheme", "pubkey"];
const canonicalBytes = (rec: Record<string, unknown>, exclude: string[]): Uint8Array => {
const trimmed = Object.fromEntries(
Object.entries(rec).filter(([k]) => !exclude.includes(k)));
return new TextEncoder().encode(canonicalJson(trimmed)); // §7.1 — ASCII-escaped
};
/** scheme "ed25519": pass a 32-byte private key. scheme "hmac-sha256": pass a secret
* string, and the per-run key is derived from it. */
export function sealEvidence(records: Record<string, unknown>[],
opts: { scheme: "ed25519"; privateKey: Uint8Array }
| { scheme: "hmac-sha256"; signingKey: string; runId: string }) {
const pubkey = opts.scheme === "ed25519"
? bytesToHex(ed25519.getPublicKey(opts.privateKey))
: bytesToHex(sha256(new TextEncoder().encode(`${opts.signingKey}:${opts.runId}`)));
const out = records.map((r) => ({ ...r } as Record<string, unknown>));
for (let i = 0; i < out.length; i++) {
if (i > 0) {
out[i].prev_sha256 = bytesToHex(sha256(canonicalBytes(out[i - 1], CHAIN_EXCLUDE)));
}
const payload = canonicalBytes(out[i], SIG_EXCLUDE);
out[i].signature = opts.scheme === "ed25519"
? bytesToHex(ed25519.sign(payload, opts.privateKey))
: bytesToHex(hmac(sha256, new TextEncoder().encode(pubkey), payload));
out[i].sig_scheme = opts.scheme;
out[i].pubkey = pubkey;
}
return out;
}
7.5 Conformance vector
Self-test your implementation against this. With signing_key = "vault-secret-example" and run_id = "run-001":
pubkey = b803b4ca95c6ea3da426ca0c4be8bad67844c9c9ac1e499192f9b1e73626fee3
Sealing these three records —
[{"step_id":"fetch_authoritative_source","ts":"2026-09-05T14:02:11Z",
"fired":true,"predicate_inputs":{"source_status":200}},
{"step_id":"compare_to_contract_terms","ts":"2026-09-05T14:02:13Z"},
{"step_id":"publish_resolution","ts":"2026-09-05T14:02:15Z"}]
— must produce exactly:
| # | prev_sha256 | signature |
|---|---|---|
| 0 | (none) | 0c91ffa4b78b01f7e4d565d76cc26db64ac9457eda3a46454db6622cf5d9678e |
| 1 | 8779deaceec478a7691921f59731ea1f8803857890840c03aef7c68e47e556c0 | c1ce1ce7a1f985831cb5bf6bbe4aaf64dffadcedb967d95180c4dd9377c0c6ca |
| 2 | bbdad7533d68dda9767596094d3b44990d016468cfdcb12d1738bb0d95c70c74 | a4fbce08d247c25d6735b7e1b969b770fc65339a5539e419fc02e486ebcab7ca |
Submitted against the declared_process in §10 this yields conformant — A1/A2/A3/A4/A6/A7 pass, A5 not_applicable, signature_binding tamper_evident_only, attestation_id 2760826254c56fb5cf379ddbc1737d37.
ed25519 vector. With the RFC 8032 seed 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bcc7cae0d8d0a over the same three records:
pubkey = 400237577b4006fc627b3fa445cecfae5700b711c85b52c8a110843d7004b0a7
| # | signature |
|---|---|
| 0 | 759453d78188a0a890e0560589de9dcdb656de0f81c2da41b5ae300c3bf011551dd567466d956f14467974efc7c2daec05bc7cea7292c652b4ca181fed522306 |
| 1 | 542cabdadb3c84591188103f352e7c809188ebbb43430e54de017aa9ba10260e4b41077d0e62519838bf659da247a6a768d97b8684cf5d0ac16c040f192a5f0f |
| 2 | c1eafd14dcca9c8b1d01e0fea28008ef578bfa3f99c82752201f30522c1c38bf4d947aafcad346bcfb2f455ec933c2004689865c8c994ff871fb62b5020c7501 |
prev_sha256 values are identical to the table above — the chain hash excludes all signature fields, so it does not depend on the scheme. Verdict: conformant, signature_binding attributable, attestation_id 96112955d196a0b82337a20ae030b059.
Negative controls. All four must hold; an implementation that passes any of them has a broken canonicalization and must not be used.
| Mutation | Required result |
|---|---|
| edit any field without re-sealing | non_conformant, A6 fail, A7 fail |
| replace a signature with zeros | non_conformant, A7 fail |
| sign with a different ed25519 key | non_conformant, A7 fail |
| malformed key/signature hex or wrong length | indeterminate, A7 unverifiable — never fail |
These vectors were produced by an implementation written from §7.1–§7.4 alone, in a different language from the reference, and checked against the live verifier; the corrected §7.1 canonicalization was separately cross-checked between TypeScript and Python on accented and astral input. That round-trip is what makes §7 publishable rather than merely descriptive.
8. ERC-8004 binding
RAP-1 works with no identity layer at all. Binding it to an ERC-8004 passport answers a different question — which resolver produced this attestation, and what is its history — and is what makes a series of attestations a track record rather than a pile of unrelated documents.
| Question | Answered by |
|---|---|
| Did the resolver follow its declared procedure on this resolution? | RAP-1 verdict + attestation_id |
| Which resolver? | ERC-8004 passport, resolved from the caller's key |
| Has it done this before, and how did those go? | the passport's accumulated attestation history |
| Can an outside party check any of it without asking us? | the recipe in §7 + the published check definitions in §6 |
attestation_id is sha256(canonical({checks, verdict}))[:32] — content-addressed, so the same inputs always produce the same id, and an id that does not reproduce is evidence the artifact was altered.
The passport is the addressable, portable half; the attestation is the per-event half. Neither is useful to a venue without the other: an identity with no record of conduct is a name, and a record of conduct with no identity cannot be attributed.
8.1 Why attestations are not written to a validation registry
They are off-chain JSON artifacts. Content-addressing (above) makes one tamper-evident once you hold it; it does not make it discoverable or third-party-timestamped.
The obvious home for that is ERC-8004's validation registry — and it does not exist yet. Checked 2026-09-05 against the registry contracts curated by the 8004 team: they publish IdentityRegistry and ReputationRegistry addresses across 40+ chains, and no ValidationRegistry address on any of them, noting that this portion of the spec is "still under active update and discussion with the TEE community" and will be revised in a follow-up spec update later this year.
On Base mainnet (chain 8453) the two live registries are:
| Registry | Address | Status |
|---|---|---|
| Identity | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 | live (BlindOracle is agent 60979) |
| Reputation | 0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 | live |
| Validation | — | not deployed, spec in flux |
So this is a blocked dependency, not an omission. Two workable interim paths exist and neither is implemented here:
1. Reputation registry. giveFeedback(...) takes a trailing bytes32, which could carry an attestation_id. This overloads a reputation primitive to carry a validation artifact — expedient, and arguably a misuse that would need unwinding once the real registry lands. 2. Independent Merkle anchoring. Batch attestation ids into a root and anchor that to Base, which is what BlindOracle's existing proof-anchoring rail already does for other proof kinds. Chain-agnostic and survives the spec settling, at the cost of being our own scheme rather than a standard one.
Adopt neither on the strength of this document. Until the validation registry ships, an attestation's discoverability is whatever the parties agree to out of band, and this profile says so rather than implying an on-chain guarantee it does not provide.
9. Conformance levels
Levels describe what the evidence supports, so a resolver can state its posture without over-claiming.
| Level | Requires | Meaning |
|---|---|---|
| RAP-1-BASIC | verdict != non_conformant, A1 pass | steps declared and observed |
| RAP-1-LOGIC | BASIC + A3 pass | recorded inputs reproduce the published outcome |
| RAP-1-CHAINED | LOGIC + A6 pass | evidence is hash-linked and order-fixed |
| RAP-1-SEALED | CHAINED + A7 pass under hmac-sha256 | bundle is tamper-evident, not attributable — the verification key travels in the record and can also sign |
| RAP-1-ATTRIBUTED | CHAINED + A7 pass under ed25519 (signature_binding: attributable) | evidence is additionally bound to the holder of a private key |
There is deliberately no level that asserts the resolution was correct. No combination of these checks can support that claim: the evidence is submitter-supplied (a fabricated but consistent log scores identically), and the predicate grammar cannot express judgment-based criteria at all.
10. Calling it
curl -s -X POST https://api.craigmbrown.com/v1/services/security.process-attestation \
-H 'Content-Type: application/json' \
-d '{
"declared_process": {
"ordered": true,
"required": [
{"id": "fetch_authoritative_source",
"predicate": {"cmp": {"field": "source_status", "op": "eq", "value": 200}}},
{"id": "compare_to_contract_terms"},
{"id": "publish_resolution"}
]
},
"run_evidence": [
{"step_id": "fetch_authoritative_source", "ts": "2026-09-05T14:02:11Z",
"fired": true, "predicate_inputs": {"source_status": 200}},
{"step_id": "compare_to_contract_terms", "ts": "2026-09-05T14:02:13Z"},
{"step_id": "publish_resolution", "ts": "2026-09-05T14:02:15Z"}
]
}'
Priced at $0.25/call over x402/USDC on Base. Malformed input returns a structured error envelope distinguishing validation (the submission) from internal (a defect in the verifier) — a verifier bug must never be reported as the submitter's fault.
The handler never raises: an unexpected fault returns an internal error rather than an HTTP 500 the caller paid for.
11. Prior evidence
A production Chainlink CRE workflow produced hash-chained, HMAC-signed evidence inside a trusted execution environment and submitted it to this verifier on 2026-09-03. Attestation 24a3f06d… returned conformant — A1 pass, A2 pass, A4 pass, A6 pass, A7 pass; 0 fail, 0 unverifiable; A3 and A5 not_applicable (no predicates or forbidden steps declared). The per-run signing key was derived from a vault secret inside the enclave, so the secret never left it.
That run is the existence proof that the format is producible under real constraints. It is not evidence that the format proves what a venue needs, for the reasons in the evidence remains submitter-supplied, and that run used the symmetric scheme, so it is tamper-evident rather than attributable.
12. Changelog
| Version | Date | Change |
|---|---|---|
| 1.0.0 | 2026-09-05 | First publication. Recipe published. |
| 1.1.0 | 2026-09-05 | ed25519 scheme added — A7 can now attribute evidence to a keyholder, reported as signature_binding. Canonicalization defect in the v1.0.0 TypeScript reference corrected (§7.1): it did not ASCII-escape non-ASCII characters, so any record containing an accented character or emoji failed A6/A7 against the verifier. Conformance vectors updated. |
13. References
- CFTC, Prediction Markets NPRM (Rule 40.11), published 2026-03-16, Federal Register 2026-05105 · CFTC
- Greenberg Traurig, CFTC Proposes New Rules for Event Contracts on Prediction Markets (June 2026)
- Norton Rose Fulbright, CFTC advances regulatory framework for prediction markets
- Congressional Research Service, CFTC Issues Proposed Rule Regarding Prediction Markets
- CoinDesk, Kraken parent Payward closes $550 million Bitnomial deal, securing full CFTC derivatives stack (2026-05-04)
- The TRADE, Kraken launches first CFTC-regulated perpetual futures for US traders
- Track360, Prediction Market Oracles & Resolution Guide 2026 (resolution models; Kalshi Outcome Review Committee)
- ERC-8004 — Trustless Agents (identity / reputation / validation registries)