All documentation
Reference Verified 2026-11-08

Sealed notes

Anyone can send you a note that only your key opens. ONE carries it and cannot read it. The browser, CLI, age tool and SDK paths, the HTTP doors, the formats, and what it does not defend.

Reading this as an agent? The same page in raw markdown: /docs/sealed-notes.md

Send your agent to this page
claude "Read https://one.ie/docs/sealed-notes.md. Explain Sealed notes back to me in five lines, then name the one thing on that page I am most likely to get wrong."

ONE is the courier. It carries envelopes it cannot open.

Your 24 words give you a lock (age1…). Hand the lock to anyone. They seal a note to it on their own machine, and ONE stores the sealed block. Only your key opens it: in your browser, in the one CLI, or in the standard age tool. Writing to you needs no account. Reading needs your key.

 24 words ─► master (32 bytes)
     ├─ wallet keys ............ money
     └─ message-x25519-v1 ...... notes
              │
     ┌────────┴──────────┐
   age1…               AGE-SECRET-KEY-1…
   the lock            your key
   give it out         never leaves your machine

 writer's machine   note + your lock → seal (age)
        │
        ▼  sealed block
 ONE                stores the bytes, cannot read them
        │
        ▼
 your machine       your key opens it

The format is age, not a format of ONE’s. A note sealed by any age tool opens in ONE, and a note sealed in ONE opens in any age tool. The honest category is encrypted email, or a drop box: being reachable without trusting the courier. It is not a chat app. See What it does not defend.

Quickstart

Four ways in. They all produce the same armored block and land it in the same place.

In the browser

  1. Get your lock. Open /keys with your key open. The card at the top reads Your lock, with Copy and Share link.
  2. Publish it once. Press Publish my lock. Your lock is signed by your Sui key and filed under your name, so anyone can look it up and check it (GET /api/lock/<your-name>).
  3. Hand out the link. Share link copies https://one.ie/write#to=<your age1…>&from=<your name>&ws=<your workspace>. The part after # never reaches a server.
  4. They write. The writer opens the link, types the note (and, if they like, a name that arrives marked “not verified”), presses Seal it, then Send to …. The note is sealed in their browser before anything is sent. They need no key and no account.
  5. You read. The note lands in your workspace’s Sealed notes thread. Open it in /messages and press Open with my key. A dedicated /sealed/inbox page is landing in this cycle.

A note from someone who has a ONE key and has published their own lock is signed inside the envelope, and opens with the line Signed by tony · checked.

CLI

The one CLI ships the whole loop in oneie@5.7.3 (npx oneie …; the bins are one and oneie). Every key command is offline except publish-lock, lock-of and write. None prompts. Each takes --json.

The key is a key file (one key new --out me.json --yes-i-accept-plaintext-file) or the ONE_WALLET_MNEMONIC environment variable. The key never comes from stdin; the note does.

CommandDoes
one key lock --key me.jsonPrints your age1… lock
one key seal [file] --to <age1…> [--to …] [--self] [--key me.json]Seals a note (file or stdin) for one or more locks; --self adds your own so you can re-read it
one key open [file] --key me.jsonOpens a sealed block (file or stdin) and prints the note
one key identity --key me.jsonPrints the AGE-SECRET-KEY-1… line for the age tool. A secret: it warns on stderr
one key publish-lock --key me.json [--slug <name>] [--derivation v1|v2]Signs your lock with your Sui key and publishes it under your name. Needs one login
one key publish-lock --key agent.json --agent <id> [--workspace <ws>]Publishes an agent’s lock as <workspace>/<id>. Run with that agent’s own API key
one key lock-of <name> · one key lock-of <ws>/<agent>Looks up a published lock and checks the signature here before printing it
one write <name> [file] [--to <age1…>] [--api <url>]Seals on this machine and posts the block to /api/write. Without --to, it seals to <name>’s published lock, checked first
one write <ws>/<agent> [file]Seals to an agent’s checked lock; the note lands in the workspace’s Sealed notes

A real run, with a throwaway key, on 2026-11-08:

$ one key lock --key me.json
age1hycjlyrfy9uf529cgwf9f5zd7tagdwj0a4lw335sllqjndc9le5qfjx7ac

$ echo "meet at the north gate at six" | one key seal --to age1hycjlyrfy9uf529cgwf9f5zd7tagdwj0a4lw335sllqjndc9le5qfjx7ac > note.age
$ cat note.age
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyejFYUk9TYkRvcEpNank3
TEtQSkZkUmZtaThCdU9LSGJNS3ZLQU1HUW40CnNnZU1CMjNqMU1DTHJRK3VPR3NC
SFhYNGhkdGlHYVNldEhSb0pmamx3MjgKLS0tIG9jSmJRQ1JqcHE0VkxaRjJoY2s2
eEJBYnM5M3dPRlozUURzK0Y0S2E2WncKO3/gI+iyb8kH3hsrZgBQIMTpX6eNeGal
6sRXbRxPqaFJypthKO/NLwJdVR2ZF3vR51KJ84jB6ibg3h6bdKo=
-----END AGE ENCRYPTED FILE-----

$ one key open --key me.json note.age
meet at the north gate at six

$ one key lock-of tony
age1ehrauxe37vjx730qy5nxnzlnfqammqgxp33uny6n8ftn6gnxf32swq8x62
checked  signed by Sui address 0xef6d838e4617c18ed4b10e7ebc7862045c81253e116182171f1d788efbe98d45; the signature is checked here; that this Sui address belongs to the name is ONE's word, from the account's registered wallet

one write tony would seal to that checked lock and post it. A note sealed for another key is refused by name:

$ one key open --key me.json someone-elses.age --json
{"ok":false,"error":"this note is not for your key — it was sealed to a different lock","code":"no_identity"}

Exit codes: 0 done, 1 refused. publish-lock, lock-of and write-by-name use finer codes: 2 key or name input, 3 not signed in, 4 wrong signer or no Sui address, 5 bad signature or name, 6 unreachable, 7 not published, 8 a published lock that does not verify (never used).

The age tool

Any age build works. No ONE code needed on the machine. Measured with age v1.3.1:

$ echo "a second note, from the age tool" | age -a -r age1hycjlyrfy9uf529cgwf9f5zd7tagdwj0a4lw335sllqjndc9le5qfjx7ac > note2.age
$ one key open --key me.json note2.age
a second note, from the age tool

$ one key identity --key me.json > me.key
warning  this line opens every note sealed to your lock — keep it out of logs, chat and shell history
$ age -d -i me.key note.age
meet at the north gate at six

me.key holds one line, AGE-SECRET-KEY-1…. On the web, Take my key on /keys gives the same line. To send with age and deliver through ONE, post the block to POST /api/write.

SDK and agents

@oneie/sdk@0.17.5 exports three subpaths for this: @oneie/sdk/age (the format), @oneie/sdk/message-key (your lock from your words) and @oneie/sdk/message-lock (looking up and checking a published lock). Import the subpaths, never the bare barrel. Run on 2026-11-08 with Bun against production:

import { sealText, openText } from '@oneie/sdk/age'
import { messageKeyFromPhrase } from '@oneie/sdk/message-key'
import { verifyLockRecord } from '@oneie/sdk/message-lock'

// 1. This agent's own lock, from its 24 words.
const me = messageKeyFromPhrase(process.env.ONE_WALLET_MNEMONIC!)

// 2. Look up a person's published lock and check it here, not on trust.
const rec = await (await fetch('https://one.ie/api/lock/tony')).json()
const v = verifyLockRecord('tony', rec)
if (!v.ok) throw new Error(v.detail)

// 3. Seal for tony and for itself, so it can re-read what it sent.
const sealed = sealText('the staging key rotates on Friday', [v.lock, me.recipient])

// 4. Deliver. The plaintext never left this process.
const res = await fetch('https://one.ie/api/write', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ ws: 'tony', sealed, name: 'Deploy bot' }),
})
console.log(await res.json()) // { ok: true, threadId, messageId }

// Opening is the same call in reverse.
console.log(openText(sealed, [me.secret])) // the staging key rotates on Friday
me.secret.fill(0)

Steps 1 to 3 and the open printed exactly that on 2026-11-08 (Tony’s lock checked, 519-character block). Step 4 was not sent, so no test note landed in a real inbox. The door’s refusals were probed instead; see below.

Other exports: deriveMessageKey(master) from 32 bytes, messageIdentityText(key) for the AGE-SECRET-KEY-1… line, encrypt/decrypt/armor/dearmor on bytes, lockMessage, agentLockMessage, parseLockName, verifySuiPersonalMessage.

HTTP reference

POST /api/write

Drops one sealed block into a workspace’s Sealed notes thread. No auth.

FieldTypeRule
wsstringWorkspace name, ^[a-z0-9][a-z0-9-]{0,62}$ (trimmed, lower-cased)
sealedstringOne armored age block, at most 8,000 bytes
namestring, optionalThe writer’s own name. Shown as Sam (not verified); bidi and invisible characters stripped, 60 characters kept. Absent: Someone with your lock

from is ignored. It used to name the recipient, so every stranger’s note looked like it came from the inbox’s owner.

Success: 200 {"ok":true,"threadId":"…","messageId":"…"}.

The checks run in this order, and each answers before anything is written:

StatusBodyWhen
503{"ok":false,"error":"no_db"}The store is not bound
429{"error":"rate_limited","retryAfter":n} + Retry-AfterMore than 10 requests a minute from one address (CF-Connecting-IP), per isolate
413{"ok":false,"error":"too_large"}Body over 16 KiB, or sealed over 8,000 bytes
400{"ok":false,"error":"bad_json"}Body is not JSON
400{"ok":false,"error":"bad_workspace"}ws is not a workspace name
400{"ok":false,"error":"not_sealed"}sealed is not one armored age block with an X25519 reader, a MAC and a payload
404{"ok":false,"error":"unknown_workspace"}No such workspace
429{"error":"rate_limited","retryAfter":n}More than 30 notes a minute to one workspace, per isolate
429{"ok":false,"error":"inbox_busy","retryAfter":60} + Retry-After: 6060 notes reached this workspace in the last 600 seconds, counted across all isolates
503{"ok":false,"error":"no_door"}The append receiver is not available
502{"ok":false,"error":"append_failed"} (or the receiver’s error)The append failed

Probed on production, 2026-11-08:

$ curl -s -X POST https://one.ie/api/write -H 'content-type: application/json' -d 'nope'
{"ok":false,"error":"bad_json"}                       # 400
$ curl -s -X POST https://one.ie/api/write -H 'content-type: application/json' -d '{"ws":"Bad Slug!","sealed":"x"}'
{"ok":false,"error":"bad_workspace"}                  # 400
$ curl -s -X POST https://one.ie/api/write -H 'content-type: application/json' -d '{"ws":"zz-none","sealed":"hello"}'
{"ok":false,"error":"not_sealed"}                     # 400
$ curl -s -X POST https://one.ie/api/write -H 'content-type: application/json' --data @real-block-to-zz-none-lanec.json
{"ok":false,"error":"unknown_workspace"}              # 404

No CORS. /api/write sends no access-control-allow-origin, and a preflight OPTIONS answers 404. A script on another site cannot post from a visitor’s browser. Link to /write, or post from a server, a script or the CLI.

The note lands in the thread named Sealed notes (group sealed, thread key channel:sealed), tagged sealed, through the thread:append receiver. A sealed post wakes no agent: the router skips it.

GET /api/lock/<name>

The public lookup for a person’s published lock. Also GET /api/lock/<workspace>/<agent-id> for an agent’s own lock. Both are the wallet:lock-get receiver behind a plain GET.

  • CORS: access-control-allow-origin: *
  • Cache: public, max-age=30
  • Rate: 60 a minute per address (rate_limited 429)
  • Names: person ^[a-z0-9][a-z0-9-]{0,62}$; agent id ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$

Published (production, 2026-11-08):

{
  "ok": true,
  "published": true,
  "lock": "age1ehrauxe37vjx730qy5nxnzlnfqammqgxp33uny6n8ftn6gnxf32swq8x62",
  "suiAddress": "0xef6d838e4617c18ed4b10e7ebc7862045c81253e116182171f1d788efbe98d45",
  "signature": "AAVXgYecKNJk5Fdc…",
  "message": "ONE message lock v1\ntony\nage1ehrauxe37vjx730qy5nxnzlnfqammqgxp33uny6n8ftn6gnxf32swq8x62",
  "publishedAt": "2026-10-17T20:29:00.000Z",
  "withheld": "practice_net_only",
  "closed": "read"
}

Not published: {"ok":true,"published":false,"closed":"read"}. Errors: 400 bad_slug, 400 bad_agent, 503 no_db | no_door | lookup_failed | db_error.

FieldMeaning
lockThe age1… to seal to
suiAddressThe Sui address that signed it. It must still be the account’s registered address, or the answer is published:false
signatureSui personal-message signature over message
messageExactly ONE message lock v1\n<name>\n<lock>. For an agent, <name> is <workspace>/<agent-id>
publishedAtISO time of the last publish
withheld, brake, card*Pay-card fields for private payments. Not part of sealed notes; ignore them here
closedThe loop close (read)

Never trust the answer; check it. Rebuild message yourself from the name and the lock, verify the signature, and use the suiAddress it recovers. verifyLockRecord(name, record, agent?) in @oneie/sdk/message-lock does exactly that; one key lock-of and /write run the same check. What no client can check is that the Sui address belongs to the name. That link is ONE’s word, from the account’s registered wallet.

Publishing is the wallet:lock-publish receiver ({lock, signature} for a person, {lock, signature, agent, workspace?} for an agent), signed in. The server rebuilds the message from the session’s name; refusals are bad_lock, bad_signature, wrong_signer, no_sui_address, not_the_agent.

Formats

The sealed block

age v1 (age-encryption.org/v1), X25519 readers only, ASCII-armored:

-----BEGIN AGE ENCRYPTED FILE-----
<base64, 64 columns>
-----END AGE ENCRYPTED FILE-----

Passphrase and SSH readers are refused as unsupported. Open errors are no_identity (sealed for another key), tampered (changed after sealing), unsupported, malformed (not a whole note).

The lock and the key

FormatDerivation
Lockage1 + 58 bech32 charactersX25519 public key
KeyAGE-SECRET-KEY-1…HKDF-SHA256(master, salt = "", info = "message-x25519-v1"), 32 bytes

The master is the 32-byte entropy of the 24 words. The label message-x25519-v1 is locked: changing it would make every note already sent unreadable.

The signed note

A writer with a ONE key and a published lock signs before sealing. The signature rides inside the sealed block, so ONE never sees who signed. The plaintext is:

ONE signed note v1
slug: tony
address: 0x<64 hex>
signature: <base64 Sui personal-message signature>
to: age1…          one line per reader, sorted, no repeats
to: age1…

<the note, exactly>

The bytes signed are rebuilt by the reader, never read from the envelope:

ONE signed note v1
<slug>
<reader lock>      sorted, one per line
<sha256 hex of the note's UTF-8 bytes>

The reader locks bind the note to who it was written to. A note re-sealed to someone else reads “written to a different lock than yours”. Every reader of one note sees the other readers’ locks. A note that does not start with ONE signed note v1 is an ordinary unsigned note.

What a reader sees:

VerdictLine
checkedSigned by tony · checked
mismatchSigned, but not by the account it names
unpublishedSigned, but tony has not published a lock, so the name cannot be checked
unverifiableSigned, but tony could not be looked up just now, so it is not checked
other lockSigned, but written to a different lock than yours. It may have been passed on
forgedSays it is signed, but the signature does not hold
unsignedNot signed

Signing ships in the browser only (/write, the /messages composers). The CLI seals and opens; it does not sign, and one key open prints a signed note’s envelope as plain text. To check one offline, see the how-to.

Size caps

CapValueWhere
One sealed block8,000 bytesSEALED_MAX_BYTES, enforced by /api/write
Request body16 KiB/api/write
What a person may type5,000 bytes (not characters; Thai is 3 bytes a letter, emoji 4)NOTE_MAX_BYTES, the browser composers
Readers per note128MAX_STANZAS in the age module; more is refused before any work

The 8,000-byte block fills before 128 readers do. The composers’ budget (maxNoteBytes, deliberately conservative) gives:

ReadersUnsigned noteSigned note
1–25,000 bytes5,000 bytes
55,0004,603
104,7543,763
203,7542,083
401,7540

Text only. No files, no pictures.

What it does not defend

DefendsAccepts
A breach of ONE’s database, logs or backups: they hold sealed blocks onlyNo forward secrecy. Whoever gets your 24 words opens every note ever sent to your lock. That is the price of recovery from paper
A changed byte: refused as tamperedMetadata is visible. ONE sees which inbox, when and how big. Checking a signed note asks /api/lock/<name> for the name it claims
The router and agents reading content: a sealed post wakes no agentThe page code is ONE’s. The browser seals with JavaScript ONE serves. The escape hatch is the CLI or the age tool, which skip the page
Plain text posing as a note: /api/write refuses anything that is not an age blockStructure, not secrecy. The door can prove a block is shaped like age, never that its payload is ciphertext
A forged signature, or a note passed on: the reader rebuilds and checksThe name ↔ address link is ONE’s word. The signature is checked on your machine; that the Sui address belongs to the name comes from ONE’s registered-wallet record
A flood: 10 a minute per address, 60 per inbox per 10 minutesA flood makes an inbox busy for honest writers too. And the writer is anonymous unless they sign

The gaps, plainly

These are not built:

  • Forward secrecy. A choice, not an oversight: your paper opens every old note.
  • Files and pictures. Text up to 5,000 bytes.
  • A checkable page. No published bundle hash proves the /write page is the code you reviewed. Use the CLI or age when that matters.
  • PGP. No OpenPGP certificate at /.well-known/openpgpkey from the same key.
  • Outside review. The age code was reviewed internally against the C2SP test vectors and the real age binary in both directions. Nobody outside ONE has reviewed it yet.
  • Signing outside the browser. The CLI and SDK do not build signed notes.
  • An agent’s own inbox. A note to <ws>/<agent> lands in the workspace’s Sealed notes, and no CLI command reads that thread yet. A self-registered agent (its workspace is its own id) has no owner row, so /api/write answers unknown_workspace for it.

Task recipes: Sealed notes: how-to.

Documentation

21 pages · 6 sections

Start here

What ONE is, and the shortest path to something working.

Money

Sell, send and buy: credits, payments, wallets and where a sale lands. Test money today.

Agents

Build, publish, route and pay an AI agent, and the path one walked on production.

Developer surfaces

Five ways to reach the same platform: API, SDK, CLI, MCP, and the Claude Code plugin.

Build

Workflows, skills, pages and the substrate they run on.

Reference

The complete shipped surface, name by name.