Sable Legacy
A Legacy plan is a sealed note and files that release to named beneficiaries when you stop checking in. You pick a check-in cadence and a grace window; optionally you name attesters who must confirm before anything releases. Each beneficiary is a wallet, and each receives a one-time claim link in advance. The link does nothing until the plan releases; then the beneficiary signs in with that exact wallet and the content is opened for them.
Every check-in, warning, confirmation, trigger, release, claim and cancellation is appended to a per-plan hash chain and anchored publicly by the same worker that anchors the Vault and agent runs. The timeline — when you last checked in, when the warning fired, who confirmed — is provable by anyone holding the event list, and the events carry nothing about the content.
What is and is not guaranteed
Say this out loud before relying on it:
- Sable holds the ciphertext under its own master key. The note and files are AES-GCM-sealed at rest, opened in-frame only to answer an authorized beneficiary's own claim, never logged, and destroyed the instant you cancel. This is the same posture as hosted agents and Relay. It is not zero-knowledge: a Sable operator with the master key and database access could read a plan's content.
- If Sable must not be able to read it, encrypt it yourself first.
Encrypt the note and files client-side (for example
ageor GPG to your beneficiaries' keys), paste the ciphertext, and setclient_encrypted: true. The flag marks the blob opaque and is shown to beneficiaries so they know they need their own key. Sable records the declaration; it cannot verify it. - Sable never holds funds or keys it can spend. A plan releases content. What that content unlocks — a hardware wallet's location, a passphrase, an account — is between you and your beneficiary.
- Release is a decision made by a clock and by people, not by proof of death. A missed check-in means a missed check-in. Choose a cadence you will keep, a grace window you can act inside, and attesters who will pick up the phone.
- Attesters are a brake, not a timer. A plan that requires N confirmations stays in warning until N arrive, however long that takes. There is no timeout on attesters. A plan with zero attesters releases on grace alone.
The state machine
active ──(check-in deadline passes)──▶ warning ──(grace passes AND
▲ │ confirmations ≥ required)
└────────── owner checks in ───────────┘ │
▼
triggered ──▶ released
any state but cancelled ──(owner cancels)──▶ cancelled
| State | Meaning | What moves it |
|---|---|---|
active | You are checking in on time. | The deadline (last_checkin_at + checkin_interval_secs) passes → warning. |
warning | A check-in was missed. Grace is running; attesters may now confirm. | Grace passes and confirmations ≥ required → released. A check-in → active, clearing every confirmation. |
triggered | Transitional; the same sweep that triggers also releases. | Immediately released. |
released | Beneficiaries can open the content with their wallet. | Cancel destroys the content (a switch that misfired can be pulled). |
cancelled | Ciphertext destroyed, claim links dead. | Terminal. |
The sweeper runs once a minute on the background-task leader and re-decides every transition under a row lock, so a check-in that lands a moment before the deadline always wins.
Quickstart
Create a plan that expects a check-in every week, waits a day after a miss, and requires one of two attesters:
curl https://api.buildsable.com/v1/legacy/plans \
-H "Authorization: Bearer $SABLE_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "If I go quiet",
"checkin_interval_secs": 604800,
"grace_secs": 86400,
"beneficiaries": [
{ "wallet": "0x1111111111111111111111111111111111111111", "label": "sister" }
],
"attesters": [
{ "wallet": "0x2222222222222222222222222222222222222222", "label": "lawyer" },
{ "wallet": "0x3333333333333333333333333333333333333333", "label": "partner" }
],
"attesters_required": 1,
"note": "The hardware wallet is in the grey safe.",
"files": [
{ "name": "recovery.txt", "content_type": "text/plain", "content_b64": "LS0tLS1CRUdJTi…" }
]
}'{
"plan": {
"id": "lg_5f1c…",
"name": "If I go quiet",
"status": "active",
"checkin_interval_secs": 604800,
"grace_secs": 86400,
"attesters_required": 1,
"last_checkin_at": "2026-09-06T10:00:00Z",
"next_deadline_at": "2026-09-13T10:00:00Z",
"client_encrypted": false,
"beneficiary_count": 1,
"attester_count": 2,
"confirmations": 0,
"object_count": 1
},
"claim_urls": [
{
"wallet": "0x1111111111111111111111111111111111111111",
"label": "sister",
"url": "https://buildsable.com/legacy/claim/…"
}
],
"note": "Claim URLs are shown once and stored only as hashes…",
"custody": "Sable holds this plan's note and files sealed under its own master key…"
}
The claim_urls are shown once — the gateway stores only a sha256 of each
token. Hand each link to its beneficiary now. Then keep checking in:
curl -X POST https://api.buildsable.com/v1/legacy/plans/lg_5f1c…/checkin \
-H "Authorization: Bearer $SABLE_SESSION_TOKEN"
A check-in resets the deadline, clears any warning, and clears every attester confirmation.
Endpoints
Owner and party endpoints are session-authed (Authorization: Bearer sess_…). The claim-link resolver is public, because a beneficiary holds only
the link.
| Method | Path | Auth | What it does |
|---|---|---|---|
| POST | /v1/legacy/plans | Session | Create a plan. Returns the plan and the one-time claim_urls. |
| GET | /v1/legacy/plans | Session | Your plans: status, timers, counts (metadata only). |
| GET | /v1/legacy/plans/{id} | Session | Detail: parties, object metadata, and the chained events with anchor state. Never content. |
| POST | /v1/legacy/plans/{id}/checkin | Session | Check in: resets the deadline, clears warning and confirmations, chains checkin. |
| PUT | /v1/legacy/plans/{id}/content | Session | Replace the note and files; re-sealed; chains content_updated. Only while active or warning. |
| POST | /v1/legacy/plans/{id}/cancel | Session | Cancel: nulls every ciphertext, kills every claim token, chains cancelled. Idempotent. |
| POST | /v1/legacy/plans/{id}/attest | Session | Confirm as an attester (one of your linked wallets must be named). Only counts while warning; idempotent per wallet. |
| GET | /v1/legacy/attesting | Session | Plans where one of your wallets is an attester: id, name, status, deadline, and whether you confirmed. Nothing else. |
| GET | /v1/legacy/claims | Session | Released plans naming one of your wallets as a beneficiary. |
| POST | /v1/legacy/claims/{plan_id}/open | Session | Open a release: the note (unsealed in-frame) and the object list. Chains claimed once per beneficiary. |
| GET | /v1/legacy/claims/{plan_id}/objects/{object_id} | Session | One file's bytes, base64. |
| GET | /v1/legacy/claim/{token} | Public | What a claim link reveals: {plan_name, wallet_required, status: "waiting" | "released"}. Unknown and cancelled tokens are a uniform 404. |
A non-attester calling attest, a non-beneficiary calling open, and a
beneficiary of a plan that has not released all receive the same 404.
Limits
name: 1–120 characters.checkin_interval_secs: 3600 to 31,536,000 (one hour to one year).grace_secs: 0 to 7,776,000 (up to 90 days). Zero means the warning and the release decision happen in the same sweep, subject to attesters.beneficiaries: 1–10 wallets (0x+ 40 hex, stored lowercase, no duplicates).attesters: 0–10.attesters_required: 0 to the number named.note: up to 64KB.files: up to 10, 5MB total, from the Relay content-type allowlist (text/plain,text/markdown,text/csv,application/json,application/pdf,image/png,image/jpeg,image/gif,image/webp). Files are stored sealed and never executed.- Active plans per account:
SABLE_LEGACY_MAX_ACTIVE_PLANS(default 10).
The event chain
Each plan carries an append-only chain. Event n hashes as
hash = sha256(prev_hash | plan_id | seq | kind | created_at)
with prev_hash empty for seq = 1 and | a literal pipe. Kinds are
created, checkin, content_updated, warning, attested, triggered,
released, claimed, cancelled. GET /v1/legacy/plans/{id} returns the
ordered events with each one's anchor (null until the anchor worker has
published it; then the anchor id, its status, and the Solana signature). The
chain is content-free by construction — nothing derived from the note or the
files enters it — so publishing it reveals only that a plan existed and when
it moved.
Webhooks
All content-free. Subscribe on the webhooks page.
| Event | Delivered to | Payload |
|---|---|---|
legacy_warning | Owner | plan_id, plan_name, warned_at, grace_secs, attesters_required, release_not_before |
legacy_released | Owner | plan_id, plan_name, beneficiaries (a count) |
legacy_claim_available | Each beneficiary wallet that maps to a Sable account | plan_id, plan_name, wallet |
legacy_cancelled | Owner | plan_id, plan_name, cancelled_at |
legacy_warning is the one to route somewhere loud: it is the signal that
you, the owner, have a grace window in which to check in.
Honest limitations
- Not zero-knowledge; see above.
client_encryptedis a declaration Sable cannot verify. - A beneficiary must hold an Ethereum-style wallet and sign in with it. There is no email or passphrase path.
- Release notifications reach a beneficiary's webhooks only if that wallet is already linked to a Sable account; otherwise the claim link they were handed is their sole channel.
- Attesters have no timeout. A plan whose attesters never confirm never releases, by design.
- The sweeper runs on the background-task leader once a minute; a transition can lag its deadline by up to that interval.