Tether

Correcting an anchor, and losing a race

Facts change. Tether does not let you overwrite one — you supersede it, and the old version stays readable as history. This guide covers the correction itself, the 409 you get when two writers race, and the one case where a correction is refused outright.

Every response below was captured from a real run.

Correct

curl -X PATCH "$TETHER/v1/anchors/$ID" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: your-unique-key' \
  -d '{"space":"default","content":"We deploy on Thursdays.","expectedVersion":1}'
{
  "id": "anc_01M2Y020S0JFARFT35C9RKYVQ5",
  "chainId": "anc_01M2Y020PWMAWH9YQMKTEK53FK",
  "version": 2,
  "stored": true,
  "searchable": false
}

The id is new; the chainId is not. A correction creates a new anchor that becomes the chain's current version. The chain id stays the id of the first version forever, which is how you follow a fact through its history.

Address subsequent corrections to the new id, and pass the version you believe is current.

History survives

curl "$TETHER/v1/anchors/$ID?space=default" -H "Authorization: Bearer $KEY"
{
  "id": "anc_01M2Y020PWMAWH9YQMKTEK53FK",
  "version": 1,
  "state": "superseded",
  "content": "We deploy on Tuesdays.",
  …
}

The original is still there, still readable by id, now marked superseded. Recall returns what is true now; direct reads can still reach what was true before.

Losing a race

expectedVersion is optimistic concurrency. Pass the version you believe is current. If someone corrected the chain first, yours is refused:

curl -X PATCH "$TETHER/v1/anchors/$V2" … -d '{… ,"expectedVersion":1}'
HTTP 409
{ "error": { "code": "stale_version", "message": "stale_version" },
  "requestId": "req_2e063497-…" }

Handle this by re-reading, not by retrying. A 409 stale_version means the fact moved underneath you. Retrying with the same body would overwrite someone else's correction with stale content; the right response is to read the current version, decide whether your change still applies, and write it against the new expectedVersion.

Why this cannot fork a chain

Worth knowing, because it explains why the guarantee is trustworthy.

The correction is a single db.batch(). It supersedes the current version, then inserts the new one. A stale expectedVersion makes the supersede match zero rows — which in D1 is not an error, so on its own the insert would commit happily and the chain would have two current heads.

What stops it is a partial unique index, one_current_per_chain, which permits exactly one current row per chain. The second insert violates it, the statement fails, and D1 rolls back every statement in the batch. The 409 you receive is that constraint firing.

Concurrency control here is a database constraint, not application statement ordering. It cannot be defeated by getting the order wrong in a later change.

Corrections that are refused, not raced

Two cases return 404, not 409:

{ "error": { "code": "not_found", "message": "not_found" }, "requestId": "req_e4f16c6d-…" }

A deleted chain. Deletion writes a marker that outlives the rows, and a database trigger refuses any insert into a marked chain. You cannot correct erased content back into existence.

A forgotten chain. Forget leaves no current head, and a second trigger refuses a current insert into a chain that has a forgotten row and no current one. You cannot correct what you can no longer read.

Both are not_found rather than something more specific, for the same reason an out-of-grant space is: telling the two apart would disclose whether a chain was forgotten or erased.

Duplicate deliveries

Every mutation requires an Idempotency-Key, and corrections are no exception. Two identical corrections sent at once — a retry, a duplicate delivery — produce one new version. The loser replays the winner's response rather than receiving a 409, because the same request succeeding twice is not a conflict.

A genuinely stale correction still gets its 409.