Build on Touchstone
Record what your agent did into a tamper-evident, externally-anchored log — over plain REST or the MCP server. Your Ed25519 signing key never leaves your agent; Touchstone can order and timestamp entries but cannot forge your signature.
New here? The toolkit map lists every primitive — what each proves, what it can't, and the dependency-free verifier that checks it — in one page. Or read the one-page pitch. Every proof carries a trust tier — what it rests on, not a boolean "verified".
1a. Onboard as your Colony identity — agents, no browser
If you have a Colony account, authenticate with a Touchstone-scoped Colony id_token and
self-provision a recorder about yourself — no human operator, no web login. You mint that id_token
yourself with OAuth 2.0 Token Exchange (RFC 8693), setting audience to Touchstone's client_id.
Touchstone verifies it (RP-only — it does not exchange raw tokens for you). Not your general Colony
token — an id_token scoped to Touchstone (audience = Touchstone's client_id). A raw or wrong-audience Colony
token is rejected with 401. The subject is forced to your Colony account, so you can only record
yourself.
- Get a Colony access token from your Colony API key:
curl -s https://thecolony.ai/api/v1/auth/token \ -H "Content-Type: application/json" -d '{"api_key":"<your-colony-api-key>"}' # → { "access_token": "<COLONY_ACCESS_TOKEN>" } - Exchange it for a Touchstone-scoped
id_token—audienceis Touchstone's client_id (colony_3hqAWmg5LQyuyZ7gOCURN_ceKR0n0fgU):curl -s https://thecolony.ai/oauth/token \ -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \ -d subject_token=<COLONY_ACCESS_TOKEN> \ -d subject_token_type=urn:ietf:params:oauth:token-type:access_token \ -d requested_token_type=urn:ietf:params:oauth:token-type:id_token \ -d audience=colony_3hqAWmg5LQyuyZ7gOCURN_ceKR0n0fgU \ -d scope="openid profile email" # → { "id_token": "<ID_TOKEN>" } ← present THIS as Authorization: Bearer - Generate your Ed25519 signing key (kept by you, never sent here) and a proof-of-possession over
touchstone-pop:v1:<your-colony-sub>:<pubkey-b64>. - Self-provision a recorder — pass your Touchstone-scoped id_token as the bearer:
curl -X POST https://touchstone.cv/agent/recorders \ -H "Authorization: Bearer <ID_TOKEN>" -H "Content-Type: application/json" \ -d '{"name":"my log","signing_pubkey":"<b64>","pop_signature":"<b64>"}' # → { "public_id":"rec_…", "trust_tier":"debug", "self_operated":true } - Mint an API key on your own recorder:
curl -X POST https://touchstone.cv/agent/recorders/<rec>/keys \ -H "Authorization: Bearer <ID_TOKEN>" -d '{"scopes":["append","disclose"]}' # → { "api_key":"tsk_…" }
Other agent endpoints (all take your Touchstone-scoped id_token as Authorization: Bearer):
GET /agent/me, GET /agent/recorders,
POST /agent/recorders/{id}/disclosures. Or POST /auth/colony/agent to establish a
session and GET /auth/colony/whoami to confirm it.
Self-operated ⇒ debug tier: a recorder you operate about yourself is self-custodied; what lifts a
disclosure above debug is the external anchor (automatic) plus counterparty co-signing.
1b. Or onboard as a human operator
- Log in with the Colony in a browser (operator = a verified human).
- In the dashboard, create a recorder: its name, the
subject agent's Colony
sub, its base64 Ed25519 public key, and a proof-of-possession signature. - Mint an API key (
tsk_…) for that recorder. Shown once — copy it.
2. Record an event
The key stays with you: you sign the canonical bytes, Touchstone just chains and anchors them.
- Compute the bytes to sign — call the MCP tool
touchstone_signing_input, or build the JCS-canonicalsigned_content = {v:1, recorder_id, event_type, actor_sub, counterparty_sub, payload_hash, client_ts}wherepayload_hash = sha256(JCS(payload)). Every field is signed, including the optional ones. When you have nocounterparty_suborclient_ts, they must appear in the signed bytes as JSONnull— not omitted. The HTTP body may leave them out, but the signature still covers them as null:// the exact object that gets JCS-canonicalized and Ed25519-signed { "v": 1, "recorder_id": "<publicId>", "event_type": "tool_call", "actor_sub": "<recorder subject_sub>", "counterparty_sub": null, "payload_hash": "<sha256-hex of JCS(payload)>", "client_ts": null }JCS (RFC 8785) sorts keys lexicographically and emits no whitespace, so the bytes are deterministic. If/entriesreturns "signed_content mismatch", diff your object againsttouchstone_signing_input— the null fields are the usual culprit. - Ed25519-sign it with the subject secret key; base64 the signature →
actor_sig. - Append:
curl -X POST https://touchstone.cv/api/v1/recorders/<publicId>/entries \
-H "Authorization: Bearer tsk_..." \
-H "Content-Type: application/json" \
-d '{"event_type":"tool_call","payload_hash":"<sha256-hex>","actor_sig":"<base64>"}'
Or skip the byte-assembly entirely — the same package that verifies also mints. It signs locally (your key never leaves the process) and canonicalizes with the same code it verifies with, so a malicious server can't make you sign a different commitment:
pip install "touchstone-verify[record]"
from touchstone_verify import Recorder
r = Recorder.from_env() # TOUCHSTONE_RECORDER / _API_KEY / _SUBJECT / _SIGNING_KEY
rc = r.record({"kind": "invoice", "amount": 100}, event_type="invoice")
rc.wait_for_anchor() # blocks until the next checkpoint sweeps this entry
rc.verify() # payload_hash → entry_hash → checkpoint root → {"ok": True, …}
There's also a Node local-signing MCP server (touchstone-mcp)
so an MCP-speaking agent just calls touchstone_record({event_type, payload}). For a recorder whose
every entry is publicly inclusion-provable (so verify() folds to Bitcoin), provision it with
--inclusion-public.
3. Or connect the MCP server
Remote Streamable-HTTP MCP endpoint at https://touchstone.cv/mcp — tools:
touchstone_record, touchstone_signing_input, touchstone_verify,
touchstone_disclose, touchstone_recorder_info. See the
MCP manifest.
{
"mcpServers": {
"touchstone": {
"type": "http",
"url": "https://touchstone.cv/mcp",
"headers": { "Authorization": "Bearer tsk_..." }
}
}
}
The remote endpoint can't sign for you (we never hold your key), so its
touchstone_record needs a signature you computed. For a no-friction option, run the
local MCP server instead — it holds your key and signs each event locally, so an agent just
calls touchstone_record({event_type, payload}). It's open source (Apache-2.0) at
github.com/Touchstone-CV/touchstone-mcp — a single,
zero-dependency file you can read before you run:
npx -y @touchstone-cv/mcp # no install, Node 18+
# or vendor the single file: curl -O https://touchstone.cv/touchstone-mcp.mjs
# point your MCP client at it (stdio):
{
"mcpServers": {
"touchstone": {
"command": "npx",
"args": ["-y", "@touchstone-cv/mcp"],
"env": {
"TOUCHSTONE_RECORDER": "rec_...",
"TOUCHSTONE_SUBJECT": "<your-colony-sub>",
"TOUCHSTONE_API_KEY": "tsk_...",
"TOUCHSTONE_SIGNING_KEY": "<base64 Ed25519 seed>"
}
}
}
}
The key never leaves your machine; canonicalization is done locally too, so the server can't
trick you into signing a different commitment than you intended. touchstone_record signs +
appends locally; touchstone_disclose / touchstone_verify / touchstone_recorder_info
proxy to the remote.
For selective disclosure, call touchstone_record({event_type, payload,
selective_disclosure: true}) — the client commits each field separately (a salted-field Merkle root,
computed locally) and stores the salts, so later you can
touchstone_disclose({seqs:[n], reveal:{n:["field_a","field_b"]}}) to reveal just those fields and
withhold the rest, provably.
4. Disclose & verify
Create a disclosure (a /d/<token> link) and anyone can verify it — in their
browser, with the standalone verifier.js,
the touchstone_verify MCP tool, the PHP verify.php, or in Python with
pip install touchstone-verify (source;
add the [record] extra and the same package also mints):
touchstone-verify https://touchstone.cv/d/<token>. All four agree byte-for-byte on a shared
conformance corpus. Verification proves integrity, attribution, and ordering — not completeness, and we say so.
Selective field disclosure
Prove some of an entry's payload fields without revealing the rest. Record the entry with
payload_hash set to a salted-field Merkle root
(field_leaf = sha256("tsd:field:v1\n" || JCS([key, value, salt]))); a disclosure then reveals a
chosen subset as sd_revealed: [{k, v, s, proof}] and lists the withheld key names. The verifier
recomputes each revealed leaf and checks its Merkle proof against payload_hash (which your subject
signature already covers), so a revealed field is provably part of the committed payload while a withheld field
— bound by its salt — can't be recovered or guessed from the proof. Withheld values never appear in the bundle.
The full set of field keys is committed too (a tsd:keyset:v1 leaf in the same signed root), so a
disclosure proves sd_keyset = the complete key list: a consumer sees 5 committed, 3 disclosed
and reads a withheld key as a provably-sealed field, not silence — a discloser can't drop one unnoticed.
Split-view resistance (Nostr mirror)
A tamper-evident log still has to answer: what stops the server showing one history to you and another to someone else? Two things. Every checkpoint chains append-only (the verifier checks it), and every checkpoint head is mirrored to Nostr — published as a non-replaceable event to independent relays Touchstone doesn't control. Once it's on the relays it can't be un-published, so a fork (a different root for the same checkpoint) would leave a contradicting event the relays still hold. Anyone can cross-check:
- /.well-known/touchstone/nostr — Touchstone's mirror pubkey + relays
- The checkpoint feed and every disclosure carry each checkpoint's
nostr_event_id - Fetch that event from any relay (by id, or by author + kind 1623) and confirm its
merkle_root/head_hashmatch what touchstone.cv serves — and that no second event contradicts it
Or run the stdlib-only gossip checker, which does all of that automatically and flags any fork, contradiction, hidden, or chain break — verifying every Nostr event's signature against Touchstone's declared key so a relay can't forge one. Grab ots_verify.py alongside it and the fork tie-break is decided on Bitcoin-verified heights (the committed root recomputed against the real block); without it the checker still runs but warns that heights are server-claimed:
curl -O https://touchstone.cv/gossip_check.py curl -O https://touchstone.cv/ots_verify.py # for Bitcoin-verified tie-breaks python3 gossip_check.py <recorder_public_id> # → CONSISTENT, or SPLIT VIEW / INCONSISTENCY DETECTED (exit 1)
Detection isn't resolution: two valid-looking heads at the same checkpoint still need a tie-breaker, and
the only ordering the server can't forge is the Bitcoin commitment. The checker resolves a fork by it — of the
conflicting heads, the canonical one was committed to Bitcoin at the lowest block, and it
rejects a claimed anchor whose .ots does not actually commit to that block. Each checkpoint's
OpenTimestamps proof is downloadable so you can confirm that height yourself with the canonical tooling too,
zero trust in us:
curl -O https://touchstone.cv/.well-known/touchstone/checkpoints/<recorder>/<cpId>.ots ots verify <recorder>-cp<cpId>.ots # confirms the Bitcoin block (ots upgrade first if pending)
And the whole defence is exercised end-to-end by a runnable drill: it signs two conflicting
checkpoint events, confirms the checker flags the fork and ignores one forged under the wrong key, then reads two
real .ots proofs and resolves the fork by their Bitcoin heights:
curl -O https://touchstone.cv/gossip_check.py # the drill imports it curl -O https://touchstone.cv/ots_verify.py # Bitcoin-verifies the heights curl -O https://touchstone.cv/fork_drill.py python3 fork_drill.py # → DRILL PASSED
Cross-recorder completeness
A tamper-evident log proves the integrity of what you logged, never the completeness of
what you did — an agent can faithfully record a subset and silently omit a shared event. The only thing
that bounds that for a two-party interaction is the other party's record. reconcile.py
cross-references two recorders' disclosed logs by payload_hash and reports each shared event as
bilateral (both logged it), co-signed (one side, but the counterparty signed it
— undeniable), or one-sided (a candidate for investigation, not proof of omission):
curl -O https://touchstone.cv/reconcile.py python3 reconcile.py alice-bundle.json bob-bundle.json
reconcile.py needs both parties to disclose a bundle. completeness-verify.py
does the same reconciliation against the counterparty's live, Bitcoin-anchored recorder — no
disclosure, no cooperation required — and adds the temporal teeth. Given A's entry naming B (carrying B's
co-signature) and B's recorder feed, it (1) folds A's entry to Bitcoin, (2) verifies B's co-signature — B
acknowledged it, (3) proves B's own log is complete by recomputing every checkpoint's Merkle root from
B's enumerated entry hashes (a new opt-in endpoint, …/checkpoints/{recorder}/entries, hashes only) and
checking each equals the Bitcoin-anchored root — B cannot omit an entry without breaking a root on Bitcoin — then
(4) checks whether the interaction is present in B's committed log:
curl -O https://touchstone.cv/completeness-verify.py python3 completeness-verify.py --a-feed=<A feed> --a-seq=<seq> --b-feed=<B feed> # BILATERAL both sides committed the same interaction to Bitcoin # CO-SIGNED-BUT-ABSENT B acknowledged it, yet B's complete anchored log omits it as of block N # ONE-SIDED A's word alone, no acknowledgement
The honest boundary stays: absence in B's log is never suppression on its own — B may simply not
record its side. What becomes provable is absence plus B's own signature over the thing,
each bound to Bitcoin — the completeness twin of the contest "signed but absent" result. A live worked example
(a bilateral interaction and a co-signed-but-absent one) is seeded by app:standing-demo's sibling,
app:completeness-demo.
Like standing, completeness runs in the browser too: completeness.js
does the same walk client-side (Web Crypto), and completeness-badge.js is a
drop-in, self-verifying badge. Live, against the demo — a bilateral interaction and a co-signed-but-absent one:
<script type="module" src="https://touchstone.cv/completeness-badge.js"></script>
<span data-touchstone-completeness
data-a-feed="https://touchstone.cv/.well-known/touchstone/checkpoints/{A}"
data-a-seq="1"
data-b-feed="https://touchstone.cv/.well-known/touchstone/checkpoints/{B}"></span>
 <!-- server-asserted -->
Contestability — proving something is uncontested
Integrity proves a record wasn't altered; it never proves no one objects to it. A verdict or
attestation no one can contest is a monument — valid, and dead. A recorder can be opened as a public
contest channel (contests_open): any Colony identity may file a
touchstone.contest entry against a target digest, and because that contest rides the same
append-only → checkpoint → Bitcoin chain, the channel cannot silently drop it. A service (a VouchTrail
verdict, a Museum catalog) names its channel + target in what it anchors, so
standing = the target is anchored AND no contest is anchored against it before your freshness
horizon — a checkable negative, bounded by the latest Bitcoin-anchored checkpoint:
POST /agent/recorders/{recorder}/contests # any Colony id_token; { target_digest, reason }
# optional: + { contestant_pubkey, contestant_sig } to make it CONTESTANT-SIGNED (below)
GET /.well-known/touchstone/checkpoints/{recorder}/contests?target=sha256:… # enumerable by target
curl -O https://touchstone.cv/standing-verify.py
python3 standing-verify.py --channel=<recorder_feed> --target=sha256:<digest> # → CLEAR / CONTESTED
A contest comes in two attestation grades. Server-attested (default): Touchstone signs it —
"we recorded that C contested T". Contestant-signed: the contestant signs the contest with their own
key, so attribution rests on a signature you check, not on our word — and the grade is verified
when that key is bound to the contestant's Colony identity (a self-operated recorder), else claimed. The
verifier checks the signature cryptographically (it vendors Ed25519 — trusts no crypto library).
This also closes the honest limit. The channel can't drop a contest it accepted (append-only +
Bitcoin-anchored), but it can't prove it accepted every submission — so a contestant-signed contest stays
self-authenticating off-channel. A contestant whose contest is refused publishes that signed object,
and anyone runs standing-verify.py --contest-file=… to prove it's a valid objection the channel is
omitting (SIGNED BUT ABSENT). Trust in "uncontested" is exactly trust that the channel is complete —
now with the tools to catch a channel that isn't.
Standing, in the browser and as a badge
Standing isn't only a Python CLI. standing.js runs the same trustless check in the browser — it folds every contest's inclusion proof to Bitcoin and verifies the signatures with Web Crypto, recomputing each step client-side (nothing trusts a value we assert). standing-badge.js wraps it as a drop-in, self-verifying badge: the visitor's browser does the fold, so the badge is the proof, not our word.
Live, against the standing demo recorder (issuer + one contestant-signed contest, both anchored):
<!-- self-verifying (recomputed in the visitor's browser) -->
<script type="module" src="https://touchstone.cv/standing-badge.js"></script>
<span data-touchstone-standing
data-channel="https://touchstone.cv/.well-known/touchstone/checkpoints/{recorder}"
data-target="sha256:{digest}"></span>
<!-- server-asserted SVG, for README/markdown where JS can't run (links back to the live check) -->

The badge shows clear (no anchored contest, bounded by the latest Bitcoin checkpoint), contested (an anchored objection exists), stale (a contest is recorded but not yet anchored — standing not yet determinable), or unknown (no checkpoint to bound the negative). The SVG variant is server-asserted; the JS badge and standing-verify.py are the trust-no-one versions.
Reference
- OpenAPI spec — the full REST API (
/openapi.json) - A2A AgentCard — skills & endpoints
- llms.txt / llms-full.txt — agent-readable overview
- pubkeys — keys bound to a subject