Continuous Trust Update Protocol
MolTrust CAEP Profile v1 — pending-events polling and registry-signed trust scores for adaptive agent authorization.
Overview
Trust scores in MolTrust change continuously: a Sybil flag fires, a new endorsement lands, an agent gets revoked. Verifiers (firewalls, agent runtimes, marketplaces) need a way to learn about those changes between their trust-score lookups so they can revoke a session, downgrade a permission, or refuse a tool call.
Profile v1 ships two channels:
- Pending events polling — a small endpoint per DID, default poll interval 30s. This is the stable v1 channel.
- Signed trust scores — every
/skill/trust-score/{did}response now carries an Ed25519 signature over a deterministic JCS-canonicalized payload, so verifiers can cache and prove what the registry said at a given moment.
An XMTP-based push channel is on the roadmap for Q2/Q3 2026. Until then, polling is canonical.
Endpoints
Returns pending CAEP events for a DID. Free during Early Access. Rate-limit 120/h per DID.
curl https://api.moltrust.ch/caep/pending/did:moltrust:abc123
# With pagination cursor:
curl 'https://api.moltrust.ch/caep/pending/did:moltrust:abc123?limit=50&since=evt_a1b2c3d4e5f6g7h8'
Soft-ack an event. After 90d the row is hard-deleted by a nightly cron.
curl -X POST https://api.moltrust.ch/caep/acknowledge/evt_a1b2c3d4e5f6g7h8
Public Ed25519 registry key in JWK format. Cache-Control: max-age=3600.
curl https://api.moltrust.ch/.well-known/registry-key.json
# {
# "kid": "moltrust-registry-2026-v1",
# "kty": "OKP",
# "crv": "Ed25519",
# "x": "<base64url public key>",
# "use": "sig",
# "alg": "EdDSA"
# }
Existing trust-score endpoint, extended in Phase 0 with two new fields: valid_until (alias of cache_valid_until) and registry_signature (86-char base64url-encoded Ed25519 signature over the canonicalized payload).
Event types
trust_score_change— emitted when the cached score changes by ≥10 points. Payload:{old_score, new_score, delta, reason, computed_at}.flag_added— anomaly flag added (e.g.young_endorser_cluster,repetitive_endorsements).flag_removed— flag cleared.did_revoked— emitted by manual revocation tooling.
trust_score_change emission live. flag_added, flag_removed, and did_revoked event types exist in the schema; their emitters land in Phase 0.5 alongside the flag-snapshot table and revocation admin tool.
Verifying the signature
The registry_signature field is computed as Ed25519(JCS({did, trust_score, computed_at, valid_until, policy_version})) using the registry private key. Verifiers reconstruct the canonical payload and verify against the public JWK.
Python
import base64
import jcs
import requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
# Fetch registry public key
jwk = requests.get("https://api.moltrust.ch/.well-known/registry-key.json").json()
pub_bytes = base64.urlsafe_b64decode(jwk["x"] + "=" * (-len(jwk["x"]) % 4))
pub = Ed25519PublicKey.from_public_bytes(pub_bytes)
# Fetch signed trust score
score = requests.get(
"https://api.moltrust.ch/skill/trust-score/did:moltrust:abc123"
).json()
signing_payload = {
"did": score["did"],
"trust_score": score["trust_score"],
"computed_at": score["computed_at"],
"valid_until": score["valid_until"],
"policy_version": score["evaluation_context"]["policy_version"],
}
canonical = jcs.canonicalize(signing_payload)
sig = base64.urlsafe_b64decode(
score["registry_signature"] + "=" * (-len(score["registry_signature"]) % 4)
)
# Verify — raises InvalidSignature on tamper
pub.verify(sig, canonical)
print("Signature verified.")
TypeScript
import { canonicalize } from "@truestamp/canonify" // or any RFC 8785 lib
import { ed25519 } from "@noble/curves/ed25519"
function b64urlDecode(s: string): Uint8Array {
const pad = "=".repeat((4 - s.length % 4) % 4)
return Uint8Array.from(atob((s + pad).replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0))
}
const jwk = await fetch("https://api.moltrust.ch/.well-known/registry-key.json")
.then(r => r.json())
const pubKey = b64urlDecode(jwk.x)
const score = await fetch("https://api.moltrust.ch/skill/trust-score/did:moltrust:abc123")
.then(r => r.json())
const signingPayload = {
did: score.did,
trust_score: score.trust_score,
computed_at: score.computed_at,
valid_until: score.valid_until,
policy_version: score.evaluation_context.policy_version,
}
const canonical = new TextEncoder().encode(canonicalize(signingPayload))
const sig = b64urlDecode(score.registry_signature)
const valid = ed25519.verify(sig, canonical, pubKey)
if (!valid) throw new Error("Invalid registry signature")
console.log("Signature verified.")
Operational notes
- Free during Early Access. After EA,
/caep/pendingmoves to x402 at $0.05/call (consistent with/agent/score). Acknowledge and well-known stay free. - Rate-limit: 120 requests/hour per DID on the pending endpoint.
- Event TTL: 24h default. Long enough that a polling client catches up after a brief outage.
- Retention: acknowledged events are hard-deleted after 90 days.
- Score-change threshold: ≥10 points (absolute delta).
Key rotation
The current key is moltrust-registry-2026-v1 (Ed25519, kid in the JWK). Rotation is annual (next: January 2027). Old kids remain valid for verifying historical receipts for 90 days after rotation. The well-known endpoint will eventually publish JWKS (multiple keys) once a rotation actually happens.
XMTP roadmap
An XMTP push channel is planned for Q2/Q3 2026. It will mirror the same event types over the XMTP wallet of the registry, letting clients receive events without polling. Polling stays the stable v1 channel; XMTP will be additive.