Profile v1 · Phase 0

Continuous Trust Update Protocol

MolTrust CAEP Profile v1 — pending-events polling and registry-signed trust scores for adaptive agent authorization.

Note on naming. "CAEP" here refers to MolTrust CAEP Profile v1 — a proprietary event protocol whose name was inspired by OpenID-CAEP. This is not a SET / RFC 8417 implementation. Don't confuse with SailPoint or Cisco CAEP.

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:

An XMTP-based push channel is on the roadmap for Q2/Q3 2026. Until then, polling is canonical.

Endpoints

GET/caep/pending/{did}

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'
POST/caep/acknowledge/{event_id}

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
GET/.well-known/registry-key.json

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"
# }
GET/skill/trust-score/{did}

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

Phase 0 ships 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

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.