# MCPFax Agent Continuity **Resume the work, not the conversation — from any agent, any provider, any runtime. A checkpoint written by one model is resumable by another.** Conversation history is a hundred pages. Work state is about three kilobytes: objective, what is done, what remains, risks, dependencies, files, evidence, next action. We do not sell memory or chat persistence — everyone has those, and every provider's version makes that provider stickier. We sell durable continuation, and the mailbox that carries work between episodic agents. Base URL: https://mcpfax-continuity.bowling-anthony.workers.dev MCP Streamable HTTP: https://mcpfax-continuity.bowling-anthony.workers.dev/mcp OpenAPI 3.1: https://mcpfax-continuity.bowling-anthony.workers.dev/openapi.json MCP registry manifest: https://mcpfax-continuity.bowling-anthony.workers.dev/server.json ## The model is email, not an alarm clock You cannot wake a stateless LLM session. It is gone. What actually happens is: event occurs -> write a task envelope to a mailbox -> the mailbox persists -> later an agent starts (for whatever reason) -> reads the mailbox -> acts -> writes results -> exits Nothing has to stay alive. Nothing polls. A scheduled time is only ONE way an item gets into a mailbox; the mailbox is the product. There is deliberately no callback URL: agents are not web services and mostly have no endpoint. ## Why this is not something a model provider will build Every provider is building memory. Each one makes THAT provider sticky, and none of them wants a workflow to survive leaving their platform. That gap is the whole point of this service, so portability here is structural, not a promise: 1. HTTP FIRST. Every tool is a plain POST with a JSON body. MCP is a wrapper over the identical code path, at the identical price. A shell script, a cron job or a local model with a curl tool is a first-class caller. You do not need MCP: curl -X POST https://mcpfax-continuity.bowling-anthony.workers.dev/v1/register -H 'content-type: application/json' -d '{}' curl -X POST https://mcpfax-continuity.bowling-anthony.workers.dev/v1/checkpoint/put \ -H "authorization: Bearer $AGENT_SECRET" -H 'content-type: application/json' \ -d '{"scope":"job-1","objective":"...","next_action":"..."}' 2. ZERO PROVIDER-SPECIFIC FIELDS. No message format, no thread ids, no framework assumptions. Plain JSON with a neutral vocabulary. Anything vendor-specific goes in `provider_extras`, which we store and return untouched and never interpret. The rule is machine-checked and reported at GET /health .portability.schema_purity. 3. CALLER-CHOSEN IDENTITY. Your address derives from your own secret, never from a session, thread or run id a provider issued. Change model, framework or runtime and the same secret reaches the same state. 4. NO ACCOUNT AND NO API KEY. x402 wallet payment means no signup, no OAuth per provider, no key rotation. An agent under any runtime pays and calls with zero onboarding. That is a bigger portability win than the schema. ## Why not the Redis you already run? An in-process lock needs a process, and an episodic agent does not have one between invocations — it is gone, and everything it held went with it. Redis, Postgres or a queue gives you storage and none of the semantics: you still write the fence tokens, the lease expiry, the dead-worker recovery, the dedupe and the retry bookkeeping, and you still operate the box. Here there is no account, no API key, no OAuth and nothing to provision, and coordination ACROSS providers has no shared backend to point at — a checkpoint written under one framework resumes under another. (This paragraph used to live in the MCP `instructions` string. It is a purchase argument for a human choosing a tool, and it was spending 600 characters of a budget an MCP host truncates at ~2048 — pushing the sentence that tells a MODEL how to pay past the cut. It belongs here, on a surface that is fetched deliberately and has no budget.) ## The headline: resume_packet `checkpoint_get` returns what was stored. `resume_packet` SYNTHESIZES a briefing for an agent starting cold: OBJECTIVE the goal, as last stated LAST VERIFIED STATE what was actually confirmed, not assumed NEW EVENTS what arrived while the agent was gone NEXT ACTION the single next step KNOWN FILES / ARTIFACTS / EVIDENCE KNOWN RISKS / OPEN QUESTIONS TOOLS ALREADY USED self-reported AND what we observed you call BUDGET REMAINING The agent gets a briefing, not an archive. The composition is the product; the storage is plumbing. ### Garbage in, garbage out — and what we do about it A packet is only as good as the checkpoint behind it. Two guards, both enforced: a. checkpoint_put is a STRUCTURED SCHEMA, not a blob. `objective` and `next_action` are REQUIRED and a checkpoint without them is REFUSED, not stored. Every other field is scored: you get a completeness figure and specific warnings on the write, so the author learns immediately, and again on the read, so the reader knows what it holds. b. We DERIVE rather than trust self-report. We already know which of our tools you called in a scope, what you claimed, which leases you still hold, where your watermarks are, and what mail arrived since your checkpoint. Those go into the packet automatically as `derived_facts`, and where they disagree with the self-reported list, the packet says to believe the observed one. Pass the optional `scope` argument to claim/lease/watermark_set/seen_add/retry_state to have that activity recorded against a unit of work. That is the one case where a key is stored in plain text rather than as a digest, it is per call, and it is opt-in. ## Getting started 1. POST https://mcpfax-continuity.bowling-anthony.workers.dev/v1/register (free) -> you get an agent_secret and a public address. Store the secret in the agent's configuration like an API key. An amnesiac agent cannot remember it for you, and we cannot recover it: we never store it. 2. Send every other request with Authorization: Bearer (MCP hosts that cannot set headers may pass agent_key as a tool argument instead). 3. POST https://mcpfax-continuity.bowling-anthony.workers.dev/v1/whoami (free) -> tells you how much work is waiting. 4. POST https://mcpfax-continuity.bowling-anthony.workers.dev/v1/inbox/poll ($0.002 per envelope first delivered) -> collect it. ## Tools ### Tier 1 — correctness (these prevent real money loss) - POST /v1/register (FREE) — Create (or re-derive) a namespace and its public mailbox address. - POST /v1/whoami (FREE) — Your agent address, registration state and how much work is waiting. - POST /v1/claim ($0.002) — First caller wins; every later caller is told it was already done. - POST /v1/complete (FREE) — Record the outcome so a later duplicate attempt gets the answer. - POST /v1/lease ($0.002) — Only one holder at a time, with an expiry so a dead holder cannot deadlock. - POST /v1/lease/renew (FREE) — Push out the expiry of a lease you still hold. - POST /v1/lease/release (FREE) — Hand the lock back before it expires. ### Tier 2 — work that otherwise gets redone - POST /v1/resume ($0.002) — A synthesized briefing for an agent starting cold on existing work. - POST /v1/checkpoint/put (FREE) — Save a structured, resumable snapshot of long work. - POST /v1/checkpoint/get ($0.002) — Fetch the stored checkpoint exactly as written, without synthesis. - POST /v1/watermark/set (FREE) — Store your position in a stream. - POST /v1/watermark/get ($0.002) — Fetch your last recorded position in a stream. - POST /v1/seen/add (FREE) — Add items to a durable seen-set. - POST /v1/seen/check ($0.002) — Return only the items you have not seen before. - POST /v1/retry-state ($0.002) — How many times this has been tried, and how long to wait. ### Tier 3 — continuity across time and across agents - POST /v1/mail/send (FREE) — Queue work in another agent's mailbox (or your own). - POST /v1/inbox/schedule (FREE) — Queue a task envelope into your OWN mailbox, now or later. - POST /v1/inbox/poll ($0.002) — Take the next due task envelope and claim it with a visibility timeout. - POST /v1/inbox/ack (FREE) — Acknowledge a polled task so it is not redelivered. - POST /v1/inbox/nack (FREE) — Return a claimed task to the queue, optionally after a delay. ### Tier 4 — what only an outside observer can do - POST /v1/heartbeat (FREE) — Record a liveness beat; if the next one is late, agreed actions run once. - POST /v1/budget/record (FREE) — Append one amount to a scope's spend ledger. - POST /v1/budget/check ($0.002) — Total, count and per-label breakdown of a scope's recorded spend. - POST /v1/barrier/create (FREE) — Wake a joiner when enough agents finish — or when the deadline passes. - POST /v1/barrier/signal (FREE) — Count one participant in; the same participant twice counts once. - POST /v1/barrier/status ($0.002) — Who has signalled, who has not, and whether the join has fired. ### Tier 5 — fan-out you did not have to write - POST /v1/work/push (FREE) — Enqueue items once, then exit. Workers pull them independently. - POST /v1/work/take ($0.002) — Claim up to n items exclusively, with a lease that returns them if you die. - POST /v1/work/done (FREE) — Complete an item you hold, optionally storing its result. - POST /v1/work/fail (FREE) — Return an item for retry, or mark it permanently failed. - POST /v1/work/status ($0.002) — Counts, results so far, and every dead-lettered item with its reason. - POST /v1/job/cancel (FREE) — Set the stop flag workers check between items. - POST /v1/job/should-continue ($0.002) — The cheap between-items check that tells a worker to stop. - POST /mcp — MCP server; tools/call is priced identically to its HTTP route. ## Cheaper and easier than writing the fan-out code yourself The alternative to tier 5 is not a competitor. It is YOU writing distributed work-distribution logic: assignment, collision avoidance, lease expiry, retry, dead-worker recovery, straggler policy. That is hard code to get right, it is never anyone's actual product, and it fails in ways that only show up in production at 3am. At $0.002 a call you can decline to own it. work_push(queue, [100 items]) parent pushes once and EXITS. free. work_take(queue, n:5) each worker pulls independently. $0.002 work_done / work_fail free. work_status(queue) done / pending / failed / dead-lettered + results so far. $0.002 on real progress. No assignment logic. No coordinator. Nothing stays alive. **Items are LEASED, not deleted.** That is the whole point. If a worker dies mid-item, its lease expires, the item goes back on the queue with `attempts` incremented, and another worker picks it up. Nothing had to be watching, because there is no orchestrator to fail. When an item exhausts `max_attempts` it is DEAD-LETTERED and surfaced by `work_status` with the error that killed it — never silently dropped. **Exclusion is the same proof as claim().** One Durable Object owns a queue and is single-threaded, so N simultaneous `work_take` calls can never be handed the same item. It is not a second mechanism bolted on; it is the mechanism this whole product is built out of. ### A barrier that waits for ALL N is a hang waiting to happen Stragglers are the normal case, not an edge case. So: barrier_create(id, expected_count:5, min_count:4, deadline_seconds:600, ...) fires when 4 of 5 report, OR at the 600-second deadline with whoever showed up, whichever comes first — and every notification NAMES the participants that did not report. If nothing fires by the TTL you still get a `barrier_timeout` envelope naming them. A timeout is information; it is delivered, not dropped. Exactly one envelope is queued whichever way it ends, because all three paths go through one deterministic dedupe key derived from the barrier's own object id. A retry, a replay or a re-fire cannot produce a second notification. ### There is otherwise no way to tell five running agents to stop job_cancel(job_id) free. sets the stop flag. should_continue(job_id) the check a worker makes between items. `should_continue` is billed **only when the answer is "no"** — that is the call that saved you money — and only the FIRST caller told to stop pays, because every later "no" is the same fact. "Keep going" told you nothing you were not already assuming, so it is free, always. We do not kill anything: a service outside your process cannot, and we will not imply otherwise. Workers cooperate by checking. ## An agent cannot detect its own death From inside a process, "I stopped" and "I am about to do the next step" are the same state. It is structurally impossible to distinguish them from the inside, which is exactly why this belongs in a service and cannot be a library: heartbeat(scope, expect_within_seconds:300, on_expiry:["release_leases","queue_alert"], notify_address:"agent:...") Beat again in time and the alarm simply re-arms. Miss it and the expiry actions run EXACTLY ONCE: leases you were holding are released through the same holder+fence path a live agent uses, one alert envelope is queued to the address you nominated, and the scope is flagged so `resume_packet` reports the death as something WE OBSERVED rather than something the dead agent claimed. `heartbeat` is free; the alert is billed only when it is collected, by the normal `inbox_poll` rule. There is no second charge. ONE LIMIT, STATED PLAINLY: `release_leases` can only reach leases you took with a `scope` argument, because a scope ledger is the only place a lease is recorded. A lease taken WITHOUT a scope is invisible to every dead-man switch — it will not be released and cannot even be counted, so a dead holder's lock blocks the work until its own TTL runs out. The action reports that blind spot on every run rather than handing back an empty list that reads like "nothing was held". Pass `scope` to every lease() you want a switch to be able to give back. Call `heartbeat(scope, disarm:true)` when the work finishes. Finishing without disarming is the usual cause of a false alarm. ## An agent cannot see its own spend either budget_record(scope, "0.002", label:"resume_packet") free, append-only budget_check(scope, window_seconds:86400) $0.002 on real history Runaway cost is invisible until the bill arrives. Amounts are **opaque decimal strings stored byte-exact** as you send them: we never convert a currency, never assume six decimals, never touch a float, and totals are summed as exact integers at the widest scale present in your own data. Set a `limit` and we report `over_limit` — **we report, we never block.** Enforcement is your decision, taken in your process with business context we do not have, and we will not pretend to an authority we do not possess. ## Pricing: free to write, paid on delivered information We bill when we tell you something that changed your behaviour. We never bill for storing, and never for telling you nothing. FREE, always: register, whoami, complete, lease_renew, lease_release, checkpoint_put, watermark_set, seen_add, send, inbox_schedule, inbox_ack, inbox_nack, heartbeat, budget_record, barrier_create, barrier_signal, work_push, work_done, work_fail, job_cancel $0.002, and only under these conditions: resume_packet: the packet carries something — a checkpoint, new events since it, or server-observed facts — and this exact briefing has not been billed before claim: granted === false, and this exact denial has not been billed before lease: acquired === false, and this refusal by this holder has not been billed before checkpoint_get: found === true, and this exact checkpoint version has not been billed before watermark_get: found === true, and this exact position has not been billed before seen_check: filtered_count >= 1, and this exact set of already-seen items has not been billed before retry_state: attempts_before_call >= 1 (remembered history from an earlier session), and this exact attempt count has not been billed before inbox_poll: an envelope was delivered for the first time (redelivery is free) budget_check: count >= 1 in the requested window, and this exact total has not been billed before barrier_status: signals >= 1, and this exact progress state has not been billed before work_take: at least one item was handed to a worker for the FIRST time (a re-take after a lease expired is free) work_status: done + failed + dead_lettered >= 1, and this exact progress state has not been billed before should_continue: continue === false, and this cancellation has not been billed before Never billed: a write; an indeterminate outcome; an invalid request; a failed settlement; a redelivery of a task_id you already paid for; a repeated denial of a fact that has not changed. When a call is not billable it still returns the full answer, with billed:false and price_charged_usdc:"0" in the payload, and no USDC moves. ## The flagship: claim() An agent that retries a failed paid call currently pays twice and has no way to know. We watched exactly that happen in our own logs: two settlements for the same /v1/weather call, four blocks apart, both charged, from a careful agent. POST /v1/claim {"key":"charge-order-8814"} -> {"granted":true} FREE. Do the work. POST /v1/complete {"key":"...","result":{}} -> stored. FREE. POST /v1/claim {"key":"charge-order-8814"} -> {"granted":false, $0.002 "first_claimed_at":..., "result":{...}} Do NOT redo it. Charging $0.002 to prevent a $0.02 double spend is the entire proposition. Retrying that same claim again is free — it is the same fact, and we do not bill twice for it. If complete() later attaches a result, the answer has genuinely changed and one further denial is billable. ## Fail closed — read this before you rely on claim() granted:true You hold the claim. Proceed. (free) granted:false It was already claimed by you in an earlier session. Do NOT proceed; use the attached result if present. granted:null INDETERMINATE. We could not determine the state, so we refused to guess. Do NOT proceed either way. Retry the identical request. HTTP 503. Never billed. A missed wake is merely late. A wrongly-granted claim is a second payment that already left your wallet. So we never return an optimistic grant. ## Delivery and billing semantics for the mailbox Delivery is AT LEAST ONCE. Billing is AT MOST ONCE per task_id. inbox_poll claims an item with a visibility timeout; ack it when you are done, or it returns to the queue and is delivered again. Every redelivery is free — you already paid for that envelope. If a settlement fails after we marked an item delivered, we roll the mark back, so the failure mode is always "we under-charged", never "we charged twice". ## Concurrency Every key routes to its own Durable Object, which is single-threaded. N simultaneous claims on one key produce exactly one granted:true. Under concurrency, exactly one of the denials is the billable one — the rest are the same fact and cost nothing. ## Agent-to-agent mail send(to, envelope) queues work in another registered agent's mailbox. A research agent finishes, mails the pricing agent, and exits. The pricing agent runs later, finds the work waiting, mails the sales agent. Nobody waits, nobody polls, everybody sleeps. Addressing: agent:<26 chars> or agent:<26 chars>/. The hard boundary: you may SEND to an address, but you may never READ a mailbox you do not hold the secret for. There is no tool that reads another agent's mailbox, lists namespaces, or enumerates keys. Because send() is free and an address is public, a mailbox's 1000-item cap is SPLIT. Ordinary mail — anyone's, including your own — stops at 968 items. The last 32 slots accept ONLY dead-man alerts and barrier notifications raised inside the namespace that owns the mailbox. So someone who knows your address can crowd out your ordinary mail for $0.00, and you will pay the normal inbox_poll price to drain it — but they can NEVER make your own dead-man alert or barrier notification undeliverable. Without that split, a free write would have been a denial of the safety features you paid for. ## Privacy: we have no dog in this fight There is no model here trained on your work state, no lock-in to protect, and no reason whatsoever for us to prefer readable data. You choose; we are indifferent. A model provider structurally cannot make that offer. A checkpoint has two parts, and the split is enforced in the schema: ENVELOPE objective, next_action, status, priority, due_by, verified_state, remaining, open_questions, risks, dependencies, files, artifacts, evidence, tools_used, budget_remaining, step. Cleartext. This is the ONLY thing we read, and the only thing resume_packet synthesizes from. BODY body, state, provider_extras. OPAQUE. Full context, reasoning, file contents, anything sensitive. Never parsed, never indexed, never logged, never in telemetry — in EVERY mode, including cleartext. Privacy mode is per CHECKPOINT, not per account: none (DEFAULT) Body stored as you sent it. We could read it; we do not. client_key You encrypt the body before sending. We store ciphertext and CANNOT read it. We never generate, receive, hold or escrow your key in this mode. resume_packet still synthesizes the full envelope briefing and hands the sealed body back verbatim for you to decrypt. escrow Reserved and NOT ENABLED. We will not claim key management we have not built. Why the default is not encryption: a lost key with no escrow is an unrecoverable first experience, and a service that silently eats your work state dies immediately. Encryption is prominent and easy, never implicit. WHERE THE KEY LIVES. Agents are stateless and cannot hold a key. Put it where durability already exists — the operator's config, e.g. a CONTINUATION_KEY environment variable or your secret manager. Every runtime can pass an environment variable; that is the one thing that persists across sessions. register returns a continuation_key ONCE if you want one; it is never stored, never logged and never accepted back. WITH client_key AND NO ESCROW, A LOST KEY MEANS THE BODY IS UNRECOVERABLE, PERMANENTLY. There is no reset, no recovery and no back door. We are not softening that. Every checkpoint and resume response carries a visibility object - { privacy_mode, envelope:"readable", body:"opaque"|"readable" } - so you never have to guess what we can see. ## What we can and cannot see CAN: payload sizes, timestamps, your namespace id and address, task ids we generated, delivery counts, which address mailed which address, and the objective/context/attachments /history blobs — stored verbatim so we can hand them back, never parsed, indexed or logged. CANNOT: your agent secret (never stored; the namespace id is its salted digest). Your idempotency keys, workflow ids, stream names, lease keys and set names — only salted SHA-256 digests are stored, so we cannot enumerate or reconstruct them. The members of a seen-set: membership can be tested, never listed. Your dedupe keys. No payload, key or secret is ever logged. There is no analytics store. ## Retention Every record carries an expiry and a Durable Object alarm deletes it; the alarm is the mechanism, not a promise. Claims 1d default / 30d max. Leases 60s / 1h. Checkpoints 30d / 90d. Watermarks 90d / 365d. Seen-sets 30d / 365d, sliding, capped at 10000 entries — when full, adds are REFUSED rather than silently forgetting an entry, because a forgotten entry would be reprocessed. Retry state 7d / 30d. Mailbox items: schedulable up to 365d ahead, dead-lettered after 5 delivery attempts or 90d undelivered, dead letters kept 7d. Tombstones 30d — they deliberately outlive the item so redelivery stays free. Full machine-readable policy: GET /health .retention ## Limits Request 65536 bytes; envelope or checkpoint 32768 bytes; key 256 chars; 100 items per seen call; 1000 pending items per mailbox, of which ordinary mail may hold 968 (the last 32 are reserved for the owner's own safety notifications). Rate limits: 240/min discovery, 120/min operations, per client address. ## Deliberately NOT built - WEBHOOKS. We deliver into a mailbox and never call you back. A service that POSTs to a caller-supplied URL is a DDoS amplifier unless it first proves the caller controls that destination, and the whole thesis here is that agents have no endpoint to be called on. - CRON EXPRESSIONS. Repeating schedules use every_seconds. Timezone and DST semantics are a correctness surface we will not fake. - IDENTITY, MEMORY, PERMISSIONS, WALLETS, CONTACTS, CALENDARS, KNOWLEDGE BASES. That is an AgentOS, and frameworks will build it natively. This is the part that works ACROSS frameworks and therefore cannot be absorbed by any one of them. - KILLING A WORKER. job_cancel sets a flag and workers cooperate by checking should_continue. A service outside your process cannot stop it, and we will not imply otherwise by calling it "cancel" and leaving you to assume. - ENFORCING A BUDGET. budget_check reports over_limit and NEVER blocks a call. Enforcement is a business decision taken in your process with context we do not have. - PRIORITIES OR FAIR-SHARE SCHEDULING inside a work queue. Items go out oldest-available first. Any other policy is one we would get wrong for someone. - CROSS-NAMESPACE QUEUES, BARRIERS OR JOBS. Sharing those would mean reading another agent's state. Workers on one queue are instances of one identity — exactly the "two copies of the same agent" case this product exists for. Cross-AGENT handoff is the mailbox, and a notify_address is written to, never read. - CURRENCY CONVERSION. A budget amount is an opaque decimal string. We do not know its unit, do not guess one, and never rescale it. - CONVERSATION OR MESSAGE HISTORY. We store work state, not transcripts. If you want a transcript, that is your provider's job and it will not survive leaving them. - READING ANOTHER AGENT'S MAILBOX, or any listing of keys or namespaces. Not an omission. ## What this does NOT solve It cannot make your agent restart. Something else must invoke it — a cron, a user, a queue, another process. We hold the work; we do not run it. It cannot stop a double spend you never claimed. claim() only helps on keys you pass it. It cannot make a non-idempotent upstream idempotent; it lets YOU be idempotent about calling one. It is not a durable execution engine: there is no replay of your code, only your state. A resume packet is only as good as the checkpoint behind it. We enforce a schema and fold in what we observed, but we cannot make an agent that writes "working on stuff" produce a useful briefing — we can only refuse the emptiest version of it and warn about the rest. We do not verify that anything in a checkpoint is TRUE. verified_state means the agent said it verified it; only the derived_facts block is our own observation. It is not a message bus: one envelope per poll, no fan-out, no subscriptions, no ordering guarantee beyond "priority first, then oldest due time" among the items that are DUE. That order is strict, not fair: a steady stream of higher-priority items CAN delay a lower-priority one indefinitely, and whoami will report it waiting the whole time. It is the right trade here because it is what puts a dead-man alert (priority 1) in front of a backlog of ordinary mail (default 5). If you need a fair drain, poll a separate box. Delivery is at-least-once, not exactly-once. Exactly-once delivery does not exist; that is what ack and the tombstone are for. It cannot stop a cancelled worker. job_cancel sets a flag; a worker that never checks should_continue will run to completion, and no service outside your process can prevent that. It cannot tell you what a budget amount MEANS. We store your digits and add them up exactly; the unit is yours and we never guess it. A dead-man switch detects SILENCE, not failure. An agent that is alive, wrong and still beating looks healthy to us, and we will not pretend otherwise.