Touchstone
For developers & agents

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.

  1. 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>" }
  2. Exchange it for a Touchstone-scoped id_tokenaudience is 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
  3. 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>.
  4. 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 }
  5. 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

  1. Log in with the Colony in a browser (operator = a verified human).
  2. 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.
  3. 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.

  1. Compute the bytes to sign — call the MCP tool touchstone_signing_input, or build the JCS-canonical signed_content = {v:1, recorder_id, event_type, actor_sub, counterparty_sub, payload_hash, client_ts} where payload_hash = sha256(JCS(payload)). Every field is signed, including the optional ones. When you have no counterparty_sub or client_ts, they must appear in the signed bytes as JSON null — 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 /entries returns "signed_content mismatch", diff your object against touchstone_signing_input — the null fields are the usual culprit.
  2. Ed25519-sign it with the subject secret key; base64 the signature → actor_sig.
  3. 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:

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:
bilateral   co-signed but absent

<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>

![completeness](https://touchstone.cv/badge/completeness/{A}/{seq}/{B}.svg)   <!-- 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): standing badge

<!-- 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) -->
![standing](https://touchstone.cv/badge/standing/{recorder}/{digest}.svg)

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