RiskRouter evidence log
Barındırılan MCP uç noktası, kurcalamaya karşı korumalı bir kanıt günlüğü için. Anahtar olmadan, imzalı başlıkları, dahil etme ve tutarlılık kanıtlarını ve kontrol noktalarını okuyun; anahtarla, bir karar kaydının tuzlanmış SHA-256'sını kaydedin. Kaydın kendisi asla sizden ayrılmaz.
Barındırılan MCP Sunucusu
npx add-mcp 'https://api.riskrouter.eu/mcp'Claude Code, Codex, Cursor ve daha fazlasına kurulur
Dokümantasyon
There is no SDK to install and nothing to license. The engine is an HTTP endpoint, the rating matrix is published at another one, and the audit ledger is a single table. Everything below runs against the live validation build right now.
Price a configuration in one request
No key, no account, no session. Post a vertical and the components the customer switched on. You get a real price. Recording that quote in the audit ledger is a separate decision, and it is the one that needs a key — take a sandbox key now, or read Keys and the audit trail first.
curl -X POST https://api.riskrouter.eu/api/v1/quote \
-H 'Content-Type: application/json' \
-d '{
"active_vertical": "urban_rental",
"selected_components": { "deposit_liquidity_swap": true, "smart_home_iot": true }
}'
You get back priced line items, a total, the normalised component state the engine used, and a separate report of whether an audit write was attempted and whether it landed.
From a Node backend
const res = await fetch('https://api.riskrouter.eu/api/v1/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
active_vertical: 'mobility',
selected_components: { commuter_roadside: true }
})
});
const quote = await res.json();
if (!res.ok || quote.ok !== true) throw new Error(quote.error?.code ?? \`HTTP ${res.status}\`);
// Show this. Never show a number your own code calculated.
const total = quote.quote.totals.total_monthly_premium; // "3.00"
// A 200 does not mean a record exists. Check the write separately.
// Without an API key this is false by design, not by failure.
const recorded = quote.persistence.persisted === true;
From Python
import requests
r = requests.post(
"https://api.riskrouter.eu/api/v1/quote",
json={"active_vertical": "micro_travel",
"selected_components": {"winter_sports_gear": True}},
timeout=10,
)
r.raise_for_status()
quote = r.json()
total = quote["quote"]["totals"]["total_monthly_premium"] # "6.00"
recorded = quote["persistence"]["persisted"] is True
Record, export and verify in five minutes, without writing to anyone
A sandbox key records quotes in the real ledger, 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 this page asks the API for your export, then checks it here: that the key is recognised, how many entries it has recorded, that the chain links up to the published head, and that we signed that head. The key is sent only to api.riskrouter.eu, is not stored, and is gone when you leave the page.
Pricing is open. Recording is attributed.
The ledger is tamper-evident: every entry seals the one before it. That is what makes it worth anything to an auditor, and it is also why the write path cannot be open. An entry written by nobody in particular is sealed in permanently, and removing it later would break every digest after it. So the engine treats three cases differently, and the third one is the point.
| You send | You get | Ledger |
|---|---|---|
| No key | A real price, immediately | Nothing written. attribution.mode is ANONYMOUS |
| A valid key | A real price | Recorded against your distributor, inside the hash chain |
| An invalid, revoked or suspended key | HTTP 401, no price | Nothing written |
A bad key is refused outright and never quietly downgraded to an anonymous quote. The alternative looks friendlier and is far worse: a distributor whose key was mistyped or revoked would keep getting 200s for months, believing they were building an audit trail, and would find out during the audit that nothing had been written. Loud now beats silent then.
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
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 write to your audit trail.
Keys are per environment. A sandbox key and a production key are separate rows and separate digests, so a test quote can never land in a production ledger by accident.
Getting your evidence back out
The same key exports it. This is the endpoint to wire into a monthly job on day one, not the day you need it.
curl -s -H "Authorization: Bearer $RISKROUTER_API_KEY" \
https://api.riskrouter.eu/api/v1/ledger/export > "ledger-$(date +%F).json"
You get your entries in full, the digest skeleton of the whole chain they sit inside, and the signed attestation for the head. Two standard-library Node scripts check all three, offline, with no key of ours and no call to us — clone github.com/aniljangrabe-dev/riskrouter-verify, a separate, public repository that shares no code with this one:
git clone https://github.com/aniljangrabe-dev/riskrouter-verify
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
Keep them. An export you downloaded last year still verifies after we have stopped answering the phone, which is the only honest answer to what happens to my audit trail 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.
Working code in the language your product is written in
Each example prices a quote, records it with an idempotency key and retries without recording twice, records a salted fingerprint of a demands-and-needs note, exports the evidence and proves one entry. Standard library only, no SDK to install, and every one is run on every change against the real engine, with its output checked by two independent verifiers.
- Python: riskrouter.py, signer.py (sign your own claims), demo.py
- PHP: RiskRouter.php, demo.php
- C# (.NET 8): RiskRouter.cs, Program.cs, RiskRouterDemo.csproj
- Java: RiskRouterDemo.java
- Reference app: app.py, a one-file broker desk that keeps the customer’s note on your side and produces an evidence pack an inspector can check offline with check_pack.py.
- Verifier: riskrouter_verify.py, standard-library Python, independent of everything else here. How to use the kit: README.
- A verification page in your own brand: verifier-page.mjs writes one HTML file, with your name and colour, that checks a receipt or a disclosure in your customer’s browser and whose security policy forbids it any network request. example-brand.json
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 alogginghandler, a decorator and an OpenTelemetry span processor, and agents.py. README - Node (20+, no dependencies):
npm install riskrouter-recorder, or the files: recorder.mjs, withwithEvidenceand 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.
Pick the one that matches your risk appetite
1. Call the API from your backend
Your server calls ours, your interface renders the result. You keep full control of the customer relationship and we never see your traffic pattern or your customer. This is what we would choose. Cross-origin calls from a browser are restricted to our own console, so this pattern is server-to-server by design.
2. Link out to the hosted console
Point a customer at the demo console to configure cover and hand the quote reference back to your flow. The fastest thing to stand up, and the least control: the page is ours, so its appearance and copy change when ours do.
3. Run the whole thing yourself
Three files and one table. Deploy the engine to your own Cloudflare account, point it at your own database, and we are no longer in the path at all. The source is not public today, so this starts with asking us for it. Covered in Run it yourself below.
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 actually apply. Build your interface from that response and a price change never desynchronises your checkout from the quote.
const { verticals } = await (await fetch(BASE + '/api/v1/verticals')).json();
for (const v of verticals) {
renderTab(v.key, v.label, v.description);
renderLocked(v.base); // mandatory, always on
v.addons.forEach(renderToggle); // optional modules
}
If you must cache it, cache it with an expiry and re-check on load. Our own console keeps an embedded copy for the first paint and hydrates from this endpoint immediately, and a test fails the build if the two ever disagree.
Handle these six before you ship
Each one has burned a real integration somewhere. They are cheap to handle now and expensive to discover in an audit.
| What happens | What to do |
|---|---|
| A 200 comes back but no audit record was written You sent no key, or the ledger was briefly unreachable. | Read persistence.persisted. Show the price, but never tell a customer their quote is on file unless it is literally true. persistence.attempted tells the two causes apart. |
| The customer toggles quickly Replies arrive out of order and a stale price lands last. | Tag each request with an incrementing number and ignore any reply older than the newest one you have rendered. Abort the previous request. |
| You send a component we do not price Your build is ahead of ours, or behind it. | Check ignored_components on every response. It is never empty by accident, so treat anything in it as a deployment mismatch and alert on it. |
| A 401 comes back where a 200 used to Your key was revoked, or your distributor was suspended. | Treat it as an outage, not as a validation error, and page someone. Do not strip the key and retry: that turns a loud failure into a silent one, and every quote after it disappears from your audit trail. |
| The engine does not answer Network fault or timeout. | Set a client timeout of a few seconds. Do not fall back to a locally computed price: showing a number we did not produce is how a customer gets quoted something you cannot honour. |
| You retry a recorded quote after a timeout The first attempt may have been written before the connection dropped. | Send an Idempotency-Key header — your own order or session reference works — and reuse it on every retry of that quote. The same key records at most one entry, and a retry gets that entry back with persistence.replayed: true. Without it, a retry can leave a second, permanent entry in an append-only ledger that says the quote was given twice. The same key for a different quote is refused with 409; use a new key for a new quote. |
What the engine refuses
| Limit | Value | On breach |
|---|---|---|
| Request body | 16 KB | 413 PAYLOAD_TOO_LARGE |
| Ledger write timeout | 5 s | Quote still returns, marked not persisted |
| Ledger writes, all callers | 120 / min | Quote still returns, marked not persisted |
| Browser origins | console only | Blocked by the browser, not by us |
The write ceiling is deliberately shared rather than per-caller. The ledger refuses deletion, so a flood would be permanent damage, and a cap that one caller cannot talk its way around is worth more than a generous one that can be.
Run it yourself
The platform is three files: a SQL schema, a Cloudflare Worker and a single-page console. There is no proprietary runtime and nothing phones home. Standing up your own copy takes about ten minutes once you have the source. That source is in a private repository today (the verifier, 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.
# 1. Create the audit ledger in your own Postgres or Supabase project
psql -f schema.sql
# 2. Point the engine at it and deploy to your own Cloudflare account
wrangler secret put SUPABASE_SERVICE_ROLE_KEY
wrangler deploy
# 3. Publish the console
npm run deploy:console
The rating matrix lives in one frozen object at the top of the Worker. Change a price there, run the tests, deploy. The test suite recomputes every premium from the matrix and fails if the console or the documentation disagrees with it.
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 infrastructure. It does not make anyone an insurance distributor, and it cannot stand in for the things a regulator expects you to hold.
- Your own FSMA registration, or the equivalent in your member state
- A carrier or underwriting pool willing to stand behind the cover
- Your own product approval and demand-and-needs process under the IDD
- Your own customer-facing terms, and the policy documents that go with them
We supply the routing, the pricing determinism and the audit trail. The regulatory position explains where the boundary sits and why it is drawn there.