RiskRouter evidence log

Endpoint MCP yang dihosting untuk log bukti yang tahan terhadap manipulasi. Tanpa kunci, baca kepala yang ditandatangani, bukti inklusi dan konsistensi, serta titik pemeriksaan; dengan kunci, catat SHA-256 yang diberi garam dari catatan keputusan. Catatan itu sendiri tidak pernah meninggalkan Anda.

Server MCP Terhosting

npx add-mcp 'https://api.riskrouter.eu/mcp'

Terpasang ke Claude Code, Codex, Cursor, dan lainnya

Dokumentasi

There is no SDK to install and nothing to license. You hash a record you keep, send us only the digest, and get back an entry and a proof that anyone can check without us. Everything below runs against the live validation build right now.

Record a fingerprint and prove it, in three steps

You keep the record. We receive its salted SHA-256 and a short kind, never the record, and answer with an entry in the log. The proof that the entry is in the log, under a head we signed, is checked without us. Recording needs a key: take a sandbox key below (one click, no account), then:

export RISKROUTER_API_KEY=rr_sandbox_...

# 1. Fingerprint a record you keep. Salt it, or anyone who can guess the record can confirm it.
SALT=$(openssl rand -hex 16)
DIGEST=$( (printf '%s' "$SALT"; cat decision.json) | sha256sum | cut -d' ' -f1)   # macOS: shasum -a 256

# 2. Record the digest. Only "recorded": true means there is an entry.
curl -s https://api.riskrouter.eu/api/v2/evidence \
  -H "Authorization: Bearer $RISKROUTER_API_KEY" -H 'Content-Type: application/json' \
  -d "{\"record_digest\":\"$DIGEST\",\"kind\":\"ai.decision\"}" | tee entry.json

# 3. Fetch the entry's proof and check it offline, with the public verifier.
LEAF=$(python3 -c 'import json; print(json.load(open("entry.json"))["entry"]["leaf_index"])')
curl -s "https://api.riskrouter.eu/api/v2/evidence/proof?leaf_index=$LEAF" \
  -H "Authorization: Bearer $RISKROUTER_API_KEY" > proof.json
git clone https://github.com/aniljangrabe-dev/riskrouter-verify
python3 riskrouter-verify/tools/python/riskrouter_verify.py proof.json --key riskrouter-verify/anchors/

The verifier prints two lines: that the entry is in the tree under the root the proof names, and that we signed that head. It uses no network and no key of ours beyond the public one, in a repository that shares no code with this service. Keep decision.json, the salt and entry.json together: without the record, nobody, including us, can show what it said.

From a Node backend

import { createHash, randomBytes } from 'node:crypto';
import { writeFileSync } from 'node:fs';

// Your record stays with you. Only its salted digest is sent.
const record = JSON.stringify({ case: 'dec-2292', outcome: 'refer', model: 'credit-v7' });
const salt = randomBytes(16).toString('hex');
const record_digest = createHash('sha256').update(salt + record).digest('hex');

const res = await fetch('https://api.riskrouter.eu/api/v2/evidence', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: \`Bearer ${process.env.RISKROUTER_API_KEY}\` },
  body: JSON.stringify({ record_digest, kind: 'ai.decision' })
});
const out = await res.json();

// 201 with recorded: true is the only success. A refused write is a 503 with recorded: false.
if (res.status !== 201 || out.recorded !== true) throw new Error(out.error?.code ?? \`HTTP ${res.status}\`);
writeFileSync(\`evidence-${out.entry.leaf_index}.json\`, JSON.stringify({ record, salt, entry: out.entry }));

From Python

import hashlib, json, os, secrets, urllib.request

# Your record stays with you. Only its salted digest is sent.
record = json.dumps({"case": "dec-2292", "outcome": "refer", "model": "credit-v7"})
salt = secrets.token_hex(16)
digest = hashlib.sha256((salt + record).encode()).hexdigest()

req = urllib.request.Request(
    "https://api.riskrouter.eu/api/v2/evidence",
    data=json.dumps({"record_digest": digest, "kind": "ai.decision"}).encode(),
    headers={"Content-Type": "application/json",
             "Authorization": "Bearer " + os.environ["RISKROUTER_API_KEY"]},
)
with urllib.request.urlopen(req, timeout=10) as r:   # raises on 401, 429 and 503
    out = json.load(r)
assert out["recorded"] is True

with open("evidence-%d.json" % out["entry"]["leaf_index"], "w") as f:
    json.dump({"record": record, "salt": salt, "entry": out["entry"]}, f)

For a record someone else must be able to re-hash byte for byte, use the canonical form the regime packs define: they also say which fields to record for IDD, MiFID II, the AI Act, the GDPR, DORA and others. The recorders do the canonical form, the salt and the journal for you. Nothing to integrate yet? Seal a file in the browser.

Record and verify in five minutes, without writing to anyone

A sandbox key records entries in the real log, exactly like a key we issue by hand. You get it on this page, once. We ask for nothing about you and store nothing about you: no account, no email, no name. Only a SHA-256 digest of the key is kept.

Is your key recording, and does your evidence check out?

Paste a key and the number of an entry you recorded with it (the leaf_index the log answered with). This page fetches that entry’s proof and checks it here: that the key is recognised, that the entry is yours, that it sits in the log under the head the proof carries, and that we signed that head. If the key has also recorded quotes through the pricing example, their export is checked too. The key is sent only to api.riskrouter.eu, is not stored, and is gone when you leave the page.

Reading is open. Recording is attributed.

Every entry is a leaf of an RFC 6962 Merkle tree, and every head of that tree is signed and can be saved by anyone. That is what makes an entry worth something to an auditor, and it is also why the write path cannot be open: an entry is permanent, and the distributor whose key recorded it is named inside the leaf, so who recorded it cannot be rewritten later.

You sendYou getLog
No keySigned heads, checkpoints, consistency proofs and public keys: everything needed to check a proofNothing written. A write answers 401 NOT_AUTHENTICATED
A valid keyYour entry, and the proof of any entry you recordedRecorded against your distributor, inside the leaf
An invalid, revoked or suspended keyHTTP 401Nothing written

A bad key is refused outright and never quietly treated as no key. The alternative looks friendlier and is far worse: a firm whose key was mistyped or revoked would keep sending digests for months, believing it was building evidence, and would find out during the audit that nothing had been written. Loud now beats silent then.

We store the SHA-256 of your key, never the key. That means we cannot show it to you again, cannot email it to you, and cannot recover it if you lose it; we can only revoke it and issue another. It also means a breach of our database does not hand anyone the ability to record in your name.

Getting your evidence back out

Each entry has its own proof, fetched with the same key. Fetch it when you record, and keep it with the record and the salt: the three together are the evidence, and they verify without us. Now and then, save a signed head as well; it needs no key. Every later head must be consistent with it, and GET /api/v2/evidence/consistency proves that it is, so a log rewritten after you saved the head cannot pass.

curl -s "https://api.riskrouter.eu/api/v2/evidence/proof?leaf_index=$LEAF" \
  -H "Authorization: Bearer $RISKROUTER_API_KEY" > "proof-$LEAF.json"
curl -s https://api.riskrouter.eu/api/v2/evidence/head > "head-$(date +%F).json"

Both verifiers, standard-library Python and Node, are in github.com/aniljangrabe-dev/riskrouter-verify, a separate, public repository that shares no code with this one. Keep them with your proofs. A proof you saved last year still verifies after we have stopped answering the phone, which is the only honest answer to what happens to my evidence if you go under. It is also why there is no SDK and nothing to license: a product that is hard to leave is a product that has to be, and we would rather not need that.

Handle these six before you ship

Each one is cheap to handle now and expensive to discover in an audit.

What happensWhat to do
A 503 with recorded: false The log refused the write: an outage, or a fault on our side.Nothing was recorded. Keep the record and its digest on your side, send it again later, and never mark it recorded until a 201 says so. The recorders keep a journal and do this for you.
A 429 EVIDENCE_CEILING for your key or for the whole log, or too many requests from your address.Nothing was recorded. Back off and send it again. A sandbox key records at most 50 entries a day; see Limits.
A 401 where a 201 used to come back Your key was revoked, or your distributor was suspended.Treat it as an outage and page someone. Do not drop the key and carry on: from that moment nothing you send is evidence.
A timeout, and you send it again The first attempt may have been recorded before the connection dropped.Number your records in a completeness chain (chain_tag and chain_seq beside the digest). A position already taken is refused with 409 CHAIN_OUT_OF_TURN and the next free position, so a retry cannot record twice and tells you the first attempt landed. Without a chain, a retry can leave the same digest twice: harmless, but it says you recorded it twice.
The record or the salt is lost The digest alone shows nothing to anyone.Store the record, the salt and the entry together, before you send the digest, and back them up like any record you must keep. We cannot give any of them back.
A digest of an unsalted record A short or predictable record can be guessed and the guess confirmed from its digest.Always hash a random salt of at least 16 bytes in front of the record, and keep it with the record.

What the log refuses

LimitValueOn breach
Request body16 KB413 PAYLOAD_TOO_LARGE
One batch (/api/v2/evidence/batch)500 entries, 128 KBRefused whole: nothing in it is written
Entries, whole log600 / min429 EVIDENCE_CEILING, nothing recorded
Entries, one self-serve sandbox key50 / day429 EVIDENCE_CEILING, nothing recorded
Entries, all self-serve sandbox keys together30 / min429 EVIDENCE_CEILING, nothing recorded
Requests from one address60 / min429

The entry ceilings are enforced by the database, not by this page or the Worker. The log refuses deletion, so a flood would be permanent damage, and a cap that no caller can talk its way around is worth more than a generous one that can be. The sandbox ceilings are lower so that a key we issue by hand can always record.

Working code in the language your product is written in

Each example records a salted fingerprint of a demands-and-needs note and proves that entry, and runs the pricing example too: it prices a quote, records it with an idempotency key, retries without recording twice and exports the evidence. Standard library only, no SDK to install, and every one is run on every change against the real service, with its output checked by two independent verifiers.

Evidence from where your decisions are already logged

If your system already logs its decisions, through Python’s logging, through OpenTelemetry spans, or through a function that returns a decision, a recorder turns that into evidence without an integration project. It canonicalises each record, salts it and keeps it, with its salt, in a journal on your side; only the digest and the kind are sent, in batches, to this log, to your own instance, or to none. A record is written to the journal before its digest is sent, so an outage never loses one, and it is marked recorded only when the log says so. No dependencies, and never a condition of using the API.

# Python: a log line with extra={"evidence": record} becomes evidence
log.addHandler(EvidenceHandler(Recorder(api_key=KEY, journal="evidence.jsonl")))
log.info("referred", extra={"evidence": {"kind": "ai.decision", "record_id": "dec-2292", "outcome": "refer"}})

// Node: OpenTelemetry spans with evidence.kind become evidence
provider.addSpanProcessor(new EvidenceSpanProcessor(createRecorder({ apiKey: KEY, journal: './evidence.jsonl' })));
  • Python (3.8+, standard library): pip install riskrouter-recorder, or the files: riskrouter_recorder, with a logging handler, a decorator and an OpenTelemetry span processor, and agents.py. README
  • Node (20+, no dependencies): npm install riskrouter-recorder, or the files: recorder.mjs, with withEvidence and an OpenTelemetry span processor; agents.mjs and mcp-server.mjs. README

AI agents. The same packages carry adapters for the frameworks agents are built with: a callback handler for LangChain and LangGraph (Python and JavaScript) and a tracing processor for the OpenAI Agents SDK (both languages). Each agent run becomes one record, with its tool calls, model calls, handoffs, guardrails and graph nodes as steps; each step keeps a SHA-256 of the text it handled, not the text. An MCP server gives any MCP client a record_decision tool; what it records is the agent’s own statement, and says so.

# LangGraph: every run of the graph becomes one record
graph.invoke(state, config={"callbacks": [EvidenceCallbackHandler(recorder, fields={"system_ref": "claims-agent"})]})

// OpenAI Agents SDK (JavaScript)
addTraceProcessor(new EvidenceTracingProcessor(recorder, { fields: { system_ref: 'claims-agent' } }));

bundle(record_digest) gives you the record, its salt and the proof, which a supervisor checks offline with either verifier. Both recorders are tested on every change against the real engine and the real OpenTelemetry, LangChain, OpenAI Agents and MCP SDKs. They are licensed Apache-2.0, like the verifiers, the conformance suite and the integration kit above. Both are published as riskrouter-recorder, on PyPI and npm; the files above are the same code, if you would rather copy them.

Nothing left out. Give a recorder a chain secret and every record goes in a numbered completeness chain: the recorder adds the chain fields itself and carries on from its journal after a restart, and completeness() returns the log’s signed list of the chain with every record in it, ready for an auditor to check that none is missing.

recorder = Recorder(chain_secret=os.environ["RISKROUTER_CHAIN_SECRET"], chain="kyc-desk")

At the OpenTelemetry Collector. otlp-sealer.mjs takes OTLP/HTTP JSON from a Collector like any other backend, and turns spans and log records that carry evidence.kind into records, with no library inside the application. A batch is answered only once it is in the journal, so the Collector retries anything not kept.

RISKROUTER_API_KEY=... node otlp-sealer.mjs --port 4319 --journal /var/lib/evidence/journal.jsonl --chain otel

Files kept elsewhere. A record can name a file it does not contain, such as a call recording, an invoice or a release: digest_file(path) in Python, digestFile(path) in Node, streams its SHA-256 and the file stays where it is.

Security logs. logseal.mjs seals any log stream (journalctl, tail -F, a syslog relay) segment by segment, so after an incident you can show that no sealed segment was changed or removed. The segments stay in your own archive. Sealed security logs

Connect an AI agent over MCP

The evidence log speaks the Model Context Protocol, so an AI assistant or agent framework can read the log, fetch proofs and record digests without anyone writing an integration. Add this server to any MCP client:

https://api.riskrouter.eu/mcp
{ "mcpServers": { "riskrouter": {
    "type": "http", "url": "https://api.riskrouter.eu/mcp",
    "headers": { "Authorization": "Bearer <your key, only to record>" } } } }

Reading needs no key. record_evidence takes the SHA-256 of a record you keep and a short kind, never the record, and needs your key; a sandbox key takes a minute. Each tool is the same route as in the API reference, with the same checks. There is no pricing tool. To record from inside the agent’s own process instead, with the record kept in your journal, use the local MCP server and the framework adapters above. Every client’s setup in one place: AI agents and assistants.

The pricing engine: a deterministic engine whose every answer can be recorded

RiskRouter began as a pricing engine for micro-insurance. It stays as a worked example for developers, not as an offer: a deterministic engine whose quotes can be recorded in a hash-chained ledger, the older cousin of the evidence log above. Its rating matrix is in the API reference, and the console drives it. Nothing above depends on it.

Price a configuration

No key: a real price, and nothing written. With a valid key, the quote is also recorded against your distributor, and attribution.recorded says whether it landed. An invalid key is refused with 401, never priced as anonymous.

curl -X POST https://api.riskrouter.eu/api/v1/quote \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $RISKROUTER_API_KEY" \
  -H "Idempotency-Key: order-4711" \
  -d '{ "active_vertical": "mobility", "selected_components": { "commuter_roadside": true } }'
// Two different questions. Ask both.
const priced   = quote.ok === true;                        // is this number real
const recorded = quote.attribution.recorded === true;      // is it in the ledger

Ask the engine what it charges

Do not hardcode the price list. GET /api/v1/verticals publishes every vertical, its mandatory base layer and its optional modules, with the prices the engine will apply. Build an interface from that response and a price change never desynchronises it from the quote.

What to check on every quote

  • persistence.persisted: never say a quote is on file unless it is literally true. persistence.attempted tells no key apart from a write the ledger refused.
  • ignored_components: anything in it is a component the engine does not price, so a deployment mismatch. Alert on it.
  • An Idempotency-Key on every recorded quote, reused on every retry of that quote: the same key records at most one entry, and a retry gets it back with persistence.replayed: true.
  • Never show a number your own code calculated, and never fall back to one when the engine does not answer.
  • Limits: a 16 KB body, a 5-second ledger write and 120 ledger writes a minute across all callers. Past either of the last two the quote still returns, marked not persisted.

Export the quote ledger

curl -s -H "Authorization: Bearer $RISKROUTER_API_KEY" \
  https://api.riskrouter.eu/api/v1/ledger/export > "ledger-$(date +%F).json"
node riskrouter-verify/tools/verify-attestation.mjs ledger-2026-09-20.json   # we signed this head
node riskrouter-verify/tools/verify-ledger.mjs      ledger-2026-09-20.json   # your entries produce it

You get your entries in full, the digest skeleton of the whole chain they sit inside, and the signed attestation for the head. /verify checks the same export in the browser.

Run it yourself

The service is one Worker and one PostgreSQL schema, with no proprietary runtime. A deployment kit runs that same code in your own account: on your own Cloudflare and Supabase, or anywhere containers run. Your instance signs its heads with its own key, holds its own data and calls nothing of ours: no licence server, no telemetry. The verifiers, the recorders and the conformance suite work against it exactly as they work against ours, and a sandbox key from your instance carries your instance’s address in its next steps.

The service’s source is in a private repository today (the verifiers, the recorders and the kits are what is published), so ask us for it; if running it without us is a condition of your pilot, agree how you would get it before the pilot starts. See the continuity and exit plan.

We would rather you could leave than have you locked in. An integration you can walk away from is one you can commit to.

What you bring

RiskRouter is evidence infrastructure. An entry shows that a record with that digest existed no later than the entry’s time and has not changed since. It does not show that the record is true or complete, and it cannot stand in for the things a supervisor expects you to hold.

  • The records themselves, and their salts, kept and backed up on your side for as long as your rules require
  • Your own decision about what goes in a record. The regime packs map fields to articles as orientation, not as legal advice
  • Your own authorisation where your activity needs one: an FSMA registration, or its equivalent in your member state
  • Your own terms with your customers, and your own data-protection analysis of what you record

We supply the log, the proofs and the verifiers. The regulatory position explains where the boundary sits and why it is drawn there.