FlexTrust Trust Receipt — specification v1
Normative, public (ADR-016). Served at /spec/trust-receipts. A trust receipt is FlexTrust’s signed attestation of a SIGNER’s published standing at a stated time, bound to the digest of a message the signer signed. It attests the signer, never the content, and it is an assessment, not a guarantee.
1. What a receipt says
“On commitment day D, the agent identified as subject — controlling
the key signer — held FlexTrust score S (band B, confidence C, evidence
tier E) under methodology version V, and that score is leaf P of the
daily Merkle commitment whose root is R. The signer signed a message
whose digest is M.”
FlexTrust signs exactly these facts as EIP-712 typed data. Nothing the presenting agent writes — no “purpose”, no free text — is under the FlexTrust signature.
2. Roles
- Signer (presenter): an agent that signs a message (an LOI, a bid, a delivery promise — any commitment) with its own key and buys a receipt for it. FlexTrust never holds keys.
- FlexTrust (attester): verifies the signer’s signature, resolves the signer to its credit file, takes the COMMITTED score for the day, countersigns the attestation, publishes the verification key.
- Verifier (counterparty): any agent or human holding the artifact — a bank officer reading a signed LOI, an escrow contract, a marketplace. Verification is free and needs no FlexTrust account.
3. EIP-712 typed data
Domain: { name: "FlexTrust", version: "1", chainId: 8453 } (no
verifyingContract in v1; a future on-chain verifier ships as version
“2”).
Primary type ScoreAttestation:
| Field | Type | Meaning |
|---|---|---|
| receiptId | string | UUID v4 of the receipt |
| subject | string | canonical DID of the scored agent (erc8004:<chain>:<token>, eoa:0x…, anchor:<kind>:<fp>) |
| subjectId | uint64 | FlexTrust agent id (needed to recompute the Merkle leaf) |
| signer | string | evm:0x<lowercase address> or ed25519:<64 hex> |
| messageDigest | bytes32 | digest of what the signer signed (see §4) |
| digestScheme | string | eip191, eip712 or ed25519-sha256 |
| score | uint16 | committed score, 0–1000 |
| band | string | presentation band (low, emerging, established, high) |
| confidenceBps | uint16 | confidence × 10,000 |
| evidenceTier | string | settled, reputation, attested, identity_only |
| methodologyVersion | string | e.g. v1.2 |
| commitmentDay | string | ISO date of the daily commitment |
| commitmentRoot | bytes32 | 0x + the day’s Merkle root (sha256 hex) |
| leafPosition | uint32 | the subject’s leaf position that day |
| issuedAt | uint64 | unix seconds |
| validUntil | uint64 | unix seconds — commitment time + 24 h (the day-frozen period) |
| keyId | string | id of the FlexTrust attestation key used |
Deliberately excluded: percentile (drifts daily), exposure-dependent risk bands and flags, any presenter-supplied text.
4. The message digest
| Scheme | Signer kind | messageDigest |
Signer signature |
|---|---|---|---|
| eip191 | evm | keccak256("\x19Ethereum Signed Message:\n" ‖ len ‖ message) |
personal_sign, 65 bytes |
| eip712 | evm | the typed-data hash the agent signed | 65 bytes |
| ed25519-sha256 | ed25519 | sha256(message) |
64 bytes |
FlexTrust stores the digest only, never the message. Issuers may submit the message (eip191, ed25519) or just the digest (eip712, or eip191 without disclosing the text).
5. The artifact
JSON, self-contained:
schema, receipt_id, url,
attestation — the 17 fields above, exactly as typed
domain, types, primary_type
issuer — { name, well_known, address, key_id }
attestation_signature — 0x + 65 bytes (r‖s‖v)
signer_signature — as submitted
proof — { leaf: {agent_id, value, computed_at, salt}, leaf_format, position, siblings[], pairing, root, committed_on }
status — valid | expired | revoked (unsigned, informational)
disclaimer
verify — { human, json, commitments, well_known }
Compact form for footers and QR codes: FTR1. + base64url(JSON
without types, verify, disclaimer, primary_type).
6. Verification (offline, without FlexTrust)
- Window:
validUntil> now. A receipt speaks for its commitment day only. - Signer: recover/verify
signer_signatureovermessageDigestperdigestScheme; it must equalattestation.signer. If you hold the message, recompute the digest from it and compare. - Attestation: hash
attestationas EIP-712 with the domain and types; ecrecoverattestation_signature; the address must appear in/.well-known/flextrust.json→attestation_keys[]with the samekey_id, andissuedAtmust fall inside that key’s validity (before anycompromised_at). - Merkle:
leaf = sha256(agent_id ‖ "|" ‖ value ‖ "|" ‖ computed_at ‖ "|" ‖ salt); foldsiblingsfromposition(even index →sha256(node ‖ sibling), odd →sha256(sibling ‖ node), odd node at a level pairs with itself); the result must equalcommitmentRoot, andleaf.valuemust equalscore,leaf.agent_idmust equalsubjectId,positionmust equalleafPosition. - Root: the root for
commitmentDayinGET /commitments.json?day=…(or your own copy of the daily roots) must equalcommitmentRoot. - Revocation (online, optional):
GET /api/v1/trust_receipts/<id>returnsrevoked_atand live flags an offline check cannot see (revoked registration, owner suspension, open disputes, a confirmed manipulation pattern).
Steps 1–5 need nothing from FlexTrust beyond two public, cacheable documents. FlexTrust’s own verifier implements exactly these steps.
7. Issuance
POST /api/v1/trust_receipts — paid like a score query (API key unit or
x402), body:
{ "signer": { "kind": "evm" | "ed25519", "identifier": "0x…" | "<hex/base64 pubkey>" },
"scheme": "eip191" | "eip712" | "ed25519-sha256",
"signature": "…",
"message": "…" // eip191 / ed25519 (optional for eip191)
"signed_digest": "0x…" // eip712, or eip191 without the message
}
Responses: 201 the artifact · 422 unknown signer / signature does
not verify · 409 the subject is not in a daily commitment yet, or
today’s commitment is stale · 503 FlexTrust is not issuing (attestation
key not configured). MCP twins: issue_trust_receipt (one unit),
verify_trust_receipt (free).
The signer must already be known to FlexTrust: a registration owner
address, a shadow (eoa:) agent, or an anchored key
(POST /api/v1/anchors, free).
8. Keys and rotation
The attestation key is secp256k1, held only in FlexTrust’s encrypted
credentials, and published as an address with a key_id and validity
window at /.well-known/flextrust.json. Rotation adds a new key and
retires the old one with retired_at; receipts issued under a retired
key stay verifiable. A compromised key carries compromised_at;
verifiers reject receipts with issuedAt at or after it, and FlexTrust
revokes them. The EIP-712 domain separates attestations from anything
else the key could sign.
9. What a receipt is not
Not a guarantee of performance, not an endorsement of the signed content, not custody, not an identity assertion beyond “this key controls this file”, not a substitute for an online check where revocation matters. The disclaimer travels with the artifact.