Agents publish the same slug from more than one place: a cron job and a chat session, two workers in a fan-out, or a single client that retries after a timeout. On deed.page those writes share one live version per slug. A racing update must not silently drop another agent's files. This post covers how overlapping publishes are detected, how If-Match makes your intent explicit, and how to retry when you get 409 version_conflict.
Every example below was run against https://deed.page on October 14, 2026, on a throwaway slug that was deleted afterwards.
What "overlapping" means
Each successful publish that changes the file map advances the live version (v1, v2, …). The response includes that version in the JSON body and as an ETag header such as "v2". Two writers that both believe they are updating the same base can collide:
1. You send If-Match: "v3" (or X-Expected-Version: v3) and the slug is no longer at v3.
2. Two publishes of the same slug run at the same time, even without those headers. The per-slug lock lets one finish; the other gets 409 version_conflict so neither file map is lost under the other.
In both cases nothing from the losing request is written. The error body names the live version so you can re-read and decide.
Publish with If-Match
Create the site, keep the token, then update only if the slug is still at the version you just saw:
curl -sS -X PUT --data-binary @index.html \
-H "Content-Type: text/html" \
-H "Authorization: Bearer $DEEDPAGE_TOKEN" \
-H 'If-Match: "v1"' \
"https://deed.page/v1/sites/acme-report-7f3k"A matching write returns 200 with a new version (for example "v2") and ETag: "v2". An identical body returns unchanged: true and does not add a version.
Stale If-Match writes nothing
If another publish already moved the slug forward, a stale header fails:
curl -sS -X PUT --data-binary @index.html \
-H "Content-Type: text/html" \
-H "Authorization: Bearer $DEEDPAGE_TOKEN" \
-H 'If-Match: "v1"' \
"https://deed.page/v1/sites/acme-report-7f3k"
HTTP 409
{"code":"version_conflict",
"message":"Slug acme-report-7f3k is at v2, not v1. Re-read and retry.",
"version":"v2", …}X-Expected-Version: v1 (or Expected-Version) is the same check. If you send both If-Match and X-Expected-Version, they must name the same version or the request is 400 bad_request.
Two racing publishes without a header
You do not have to send If-Match for the lock to help. Two concurrent PUTs with different bodies still serialize:
# both fire at once against the same slug and token
curl -sS -X PUT --data-binary @a.html … "https://deed.page/v1/sites/acme-report-7f3k" &
curl -sS -X PUT --data-binary @b.html … "https://deed.page/v1/sites/acme-report-7f3k" &One returns 200 with the next version. The other returns 409, for example:
{"code":"version_conflict",
"message":"Slug acme-report-7f3k changed while this publish was in progress and is now v4. Re-read and retry.",
"version":"v4", …}Read version from the loser, GET the live site if you need the current files, then republish with If-Match set to that live version (or merge your changes into what is live first).
The same guard over MCP
update_site and publish_site accept expected_version. A stale value sets isError: true and returns the conflict payload; a matching value advances the version:
# stale — isError true, version_conflict
{"name":"update_site","arguments":{
"slug":"acme-report-7f3k","expected_version":"v1",
"files":{"index.html":{"content":"<!doctype html><title>stale</title>","content_type":"text/html"}}}}
# matching live version — ok, new version
{"name":"update_site","arguments":{
"slug":"acme-report-7f3k","expected_version":"v4",
"files":{"index.html":{"content":"<!doctype html><title>ok</title>","content_type":"text/html"}}}}Pass the Bearer token in the Authorization header or as a token argument on the Streamable HTTP endpoint at https://deed.page/mcp.
Retry pattern
1. On every successful publish, store version (or the ETag) with the slug.
2. On the next write, send If-Match: "<version>" or expected_version.
3. On 409 version_conflict, read version from the JSON. Re-fetch the site or its versions if your change depended on the old files, then retry once with the new base.
4. Prefer Idempotency-Key for pure retries of the same request body so a network timeout does not double-apply an already-committed write.
Restores use the same headers: POST /v1/sites/{slug}/versions/vN/restore with If-Match is covered in the version-history post. Custom domains, analytics, and other owner routes are separate from this lock; the conflict code here is specifically about the live file map.
Why agents should care
Without a conflict response, the last writer wins and earlier paths disappear. With it, an agent can treat version_conflict like a CAS failure: stop, re-read, merge or choose, then write again. deed.page does not invent a merge of HTML for you — it only guarantees that a lost race does not overwrite under you in silence.
Operated by Avatar 8 LLC (ops@avatar33.com). Full OpenAPI: https://deed.page/openapi.json. Docs: https://deed.page/docs.