How it works
- You type a note in your browser. Text only. Pick how long it may wait unread: 1 hour, 24 hours or 7 days.
- Your browser makes a random 256-bit key and encrypts the note with AES-256-GCM. Only the ciphertext and a random IV go to the server.
- You get a link:
/n/<id>#<key>. The key sits after the#. Browsers never send that part to any server. - The reader opens the link and sees a page that says This note burns when you open it. Loading that page does not burn anything, so link previews in chats and on X are safe.
- They press Open & burn. The server hands over the ciphertext and deletes the row in the same database transaction. A second request gets
410. - The reader's browser decrypts with the key from the link and shows the text. When they close it, the text turns to ash. It was never written to storage.
- If nobody opens the note in time, the server deletes it when the timer runs out and counts it as expired unread.
The ciphertext is the only copy of the note on the server. It is removed on open or on expiry, and the database uses secure_delete so the freed bytes are overwritten.
What the server can see
The server sees the ciphertext and its size, the time it was sealed and opened, your IP address while the request is in flight (hashed for the daily quota, never stored raw), the wallet address if you lock or log in, and whether a note was opened or expired. It never sees the text or the key. The key is in the link: whoever holds the full link can open the note once, and the messenger you send it through can see the link. Your browser and the reader's browser hold the text in memory while open. We keep no backups of sealed notes.
What this means for you
- Send the full link through a channel you trust. Anyone who gets the whole link before the reader can burn it first.
- If a reader's device is compromised, or someone photographs the screen, burning cannot help. A note can only be read once, not un-read.
- Logs record the method, the route pattern, the status and the time, never the note id, the receipt id or anything after a
#.
Tiers
Free works for everyone, with no wallet. The other two use $ONCE and open at launch.
| Tier | How | Limits | State |
|---|---|---|---|
| Free | Nothing needed | 5 notes per day per IP (hashed), up to 10,000 characters, kept up to 7 days | Live |
| Holder · 1,000,000 $ONCE | Hold at least 1,000,000 $ONCE and sign a login message with that wallet. Balance is read live from the chain. | 50 notes per day per address, up to 100,000 characters, kept up to 30 days, lock to a wallet | opens at launch |
| Burn credit · 100,000 $ONCE | Send at least 100,000 $ONCE to 0x000000000000000000000000000000000000dEaD, then sign and paste the transaction hash. One transaction hash is one credit, never reused. | One note: up to 100,000 characters, kept up to 90 days, lock to a wallet | opens at launch |
Lock to a wallet
On Holder and Burn credit you can lock a note to a 0x… address. To open it, the reader signs a one-time challenge with that wallet and the server checks the signature before it releases the ciphertext. The wrong wallet gets 403 and the note is not burned. In plain words: the server refuses to hand the sealed text to any other wallet. This is access control on top of the encryption. The key still travels in the link.
Wallets
Browser wallets only (MetaMask, Rabby, or any wallet's built-in browser). Messages are signed with personal_sign; nothing is ever sent as a transaction by this site. The chain is Robinhood Chain (id 4663).
Signed messages, exactly
ONCEINK login
address: 0x<lowercase address>
issued: 2026-10-22T15:00:00Z
ONCEINK open
note: <id>
nonce: <nonce from the server>
expires: <ISO-8601 UTC>
ONCEINK credit
tx: 0x<64 lowercase hex>
issued: 2026-10-22T15:00:00Z
Separators are a single \n. issued is UTC, seconds, no milliseconds, and must be within 10 minutes of the server clock.
Seal a note from the terminal
Needs Node 22 or newer. It encrypts locally with the same format as the site (AES-256-GCM, additional data onceink:v1, base64url without padding), posts only the ciphertext and prints the link. The key never leaves your machine except inside that link.
node -e 'const c=require("crypto"),b=x=>Buffer.from(x).toString("base64url"),O="https://onceink.xyz",k=c.randomBytes(32),iv=c.randomBytes(12),a=c.createCipheriv("aes-256-gcm",k,iv);a.setAAD(Buffer.from("onceink:v1"));const ct=Buffer.concat([a.update(process.argv[1]),a.final(),a.getAuthTag()]);fetch(O+"/api/notes",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({ct:b(ct),iv:b(iv),keepSec:86400})}).then(r=>r.json()).then(j=>console.log(O+"/n/"+j.id+"#"+b(k)))' "your secret here"
It prints /n/<id>#<key> on the site's origin. Open that link in a browser and it decrypts. The free limits apply (5 per day per IP, 10,000 characters).
Format
key: 32 random bytes, 43 characters of base64url. It is the fragment, nothing else.iv: 12 random bytes, 16 characters, fresh for every note.ct: base64url of ciphertext followed by the 16-byte tag. Size caps are onctlength: 56,000 characters free, 560,000 for Holder and Burn credit.- Plaintext is the UTF-8 bytes of the text, with no wrapper.
API
JSON over HTTPS, same origin. Every error looks like {"error":"code","message":"plain words"}. All responses are Cache-Control: no-store. Ids are 22 characters of base64url.
| Method and path | Request | Response |
|---|---|---|
| POST /api/notes | {ct, iv, keepSec, lockAddr?, tierToken?, creditToken?} | 201 {id, receiptId, expiresAt, tier}. keepSec is 3600, 86400 or 604800; plus 2592000 for Holder; plus 7776000 for Burn credit. Errors: 400 bad_request, 401 bad_token, 403 not_holder, 409 credit_used, 413 too_big, 429 quota |
| GET /api/notes/:id/meta | none | 200 {exists, locked, lockHint, expiresAt}. Never burns and never returns ciphertext. 410 burned with kind; 404 not_found |
| GET /api/notes/:id/challenge | none | 200 {nonce, message, expiresAt} for locked notes. 400 not_locked; 404; 410 |
| POST /api/notes/:id/open | {nonce?, sig?} | 200 {ct, iv, burnedAt, receiptId} and the note is deleted. 403 wrong_wallet, bad_signature, challenge_used, challenge_expired or locked (none of these burn); 404; 410 |
| POST /api/tier/login | {address, message, sig} | 200 {tierToken, tier, balance, expiresAt}. 403 not_holder; 400 bad_message; 409 not_launched; 503 chain_unreachable |
| POST /api/tier/burn | {txHash, message, sig} | 200 {creditToken, burned, expiresAt}. 404 tx_not_found; 400 no_burn_log or below_threshold; 403 not_sender; 409 credit_claimed or not_launched; 503 |
| GET /api/receipts/:rid | none | 200 {status, sealedAt, expiresAt, burnedAt?, id?, ctHash?, leaf?, proof?, batch?}; 404 |
| GET /api/furnace | none | 200 {burned, expired, recent:[{leaf12, at, kind, block?}]} |
| GET /api/furnace/stream | none | Server-sent events: hello, burn, anchor |
| GET /api/proof | none | 200 {wallet, status, pending, batches}, newest 100 batches |
| GET /healthz | none | 200 {ok:true} |
Rate limits apply per IP. A quota 429 (5 free notes a day) includes resetAt.
Burn receipts and the anchor
Every note gets a receipt id when it is sealed. The receipt page shows when it was sealed, whether it was burned by the reader or expired unread, and when. At burn time the server records one 32-byte leaf. The leaf is a hash of the note id, the ciphertext hash, the burn time and the kind. It contains nothing about the text or the key.
Every 10 minutes, if there are new leaves, the server builds one Merkle root and writes it to Robinhood Chain in a zero-value transaction from the server wallet to itself, with the calldata ONCEINK:v1:<root>. That is the whole receipt system: a public, timestamped fingerprint that a note was burned. If the wallet runs low, the next batch waits and the page says so. The Proof page lists every batch and checks a receipt in your browser.
leaf = sha256( id(16 bytes) || sha256(ct bytes) || uint64be(burnedAt seconds) || kind )
kind = 0x01 opened, 0x02 expired
parent = sha256( min(a,b) || max(a,b) ) odd last node is promoted unchanged
FAQ
What if the server is seized?
Whoever has the disk finds only ciphertext for notes that are still sealed, and no keys: the keys live in links, not on the server. Notes that were already opened or expired are deleted, and the freed bytes are overwritten.
Do you keep backups?
No. The database file is never backed up. A note that is gone from the server is gone.
I lost the link. Can you recover the note?
No. The key exists only inside the link, so nobody can decrypt a note without it, including us. Seal a new note instead.
The link was opened by someone else first.
Then it is burned, and your reader sees Already burned with the time. The receipt shows the burn time. If the link travelled through a messenger or a group, assume that service could see it.
The reader says the link is incomplete.
The part after # was cut off. Some apps trim it. Copy the whole link again, or send it in a plain text message.
What happens to a locked note if the wrong wallet tries?
The server answers 403 and keeps the note. Nothing is burned. The right wallet can still open it until the timer runs out.
Can the text be recovered after burning?
No. After the reader opens it, the server has no copy. The reader's browser holds the text in memory until it is closed and it is never saved by the page.
What do the receipts and the anchor prove?
They prove that a note with that id and ciphertext hash was burned at that time, and that the record was not changed after the batch was written to the chain. They do not reveal or prove anything about the content.
What does the site store in my browser?
Nothing about notes. The text is kept in memory only while it is on screen.
Is this advice or a promise about $ONCE?
No. $ONCE unlocks the tiers above and nothing else here. It makes no promise about price or returns.