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.