SINGENOTE
Launches Sun 1 Nov
CA 0x0000…0000

Documentation

How SINGENOTE works

Paste a secret. Share the link. It burns after one read. Encrypted in your browser — the server only ever stores ciphertext.

Overview

SINGENOTE is a one-time note service. You paste text, your browser encrypts it, and you get a link. The first person to open the link and press Open & burn reads the note; at that moment the server deletes it. Anyone who opens the link afterwards sees “This note is gone.”

There are no accounts, no cookies and no wallet needed for the basic flow. Notes that nobody opens expire on their own after 1 hour or 24 hours (7 days for holders).

How it works

  1. Seal. Your browser generates a random 256-bit key and a 96-bit IV with crypto.getRandomValues, derives an AES-256-GCM key and encrypts the text. Only the ciphertext, the IV and the expiry are uploaded (plus, for notes with a passphrase, a random salt and a verifier that cannot decrypt anything).
  2. Share. The link looks like https://singenote.pro/n/<id>#<key>. Everything after # is the URL fragment, which browsers never send to servers. The server knows the note id; it never sees the key.
  3. Burn. Opening the page only reads metadata (does the note exist, does it need a passphrase). The note is consumed by a POST sent when the reader presses the button. The server selects the row, checks the passphrase verifier if there is one, and deletes the row in a single SQLite transaction, returning the ciphertext to exactly one caller. The reader's browser decrypts it locally.

Link previews can't burn a note: unfurl bots fetch the page with GET, and GET never deletes anything.

Security model

All binary values are base64url without padding. Format version v: 1.

ValueSizeHow it's made
K_link32 bytesRandom. Lives only in the link fragment.
iv12 bytesRandom per note.
salt16 bytesRandom, only when a passphrase is set.
K_pass32 bytesPBKDF2-HMAC-SHA-256(passphrase NFC, salt, 300,000 iterations).
K_enc32 bytesHKDF-SHA-256(K_link ‖ K_pass?, salt or 16 zero bytes, info singenote/v1/enc).
verifier32 bytesPassphrase notes only: HKDF-SHA-256(K_pass, salt, info singenote/v1/check).
cttext + 16 bytesAES-256-GCM(K_enc, iv, AAD singenote/v1, 128-bit tag) over the UTF-8 text.

Why a wrong passphrase doesn't burn the note

With a passphrase, the reader's browser derives a verifier and sends it with the open request. The server compares it in constant time inside the same transaction as the delete. A mismatch returns 403 and the note stays intact. Five wrong attempts lock that note for 10 minutes.

The verifier is derived from the passphrase only, with a different HKDF label than the encryption key, and contains no bits of K_link. Even someone who reads the database and guesses the passphrase still can't decrypt the note without the link.

Passphrase links carry the salt next to the key: #<key>.<salt>. The salt is not secret; it lets the reader's browser build the verifier before anything is deleted.

Tampered links

If the key in the fragment is altered by hand, the server can't tell — it hands over the ciphertext and deletes it, and decryption fails with “Couldn't decrypt. The link key is wrong, and the note has already been burned.” This is the only path where a note is lost without being read.

What this does NOT protect against

Limits & tiers

TierText sizeExpiryNeeds
Free1,024 bytes (UTF-8)1 hour or 24 hoursNothing
Holder10,240 bytes (UTF-8)1 hour, 24 hours or 7 days≥ 500,000 tokens, message signature

Sizes count bytes, not characters: emoji and non-Latin scripts use more than one byte each. Rate limits per IP: 20 new notes per 10 minutes, 120 metadata checks and 60 open attempts per 10 minutes.

API

The API only ever handles ciphertext. You can script it, but the encryption must happen on your side with the format above — sending plaintext here would defeat the point.

# create (ciphertext only)
curl -s https://singenote.pro/api/notes \
  -H 'Content-Type: application/json' \
  -d '{"v":1,"ct":"<base64url>","iv":"<16 chars>","ttl":86400,"salt":null,"verifier":null}'
# → 201 {"id":"<22 chars>","expiresAt":"…","tier":"free"}

# metadata (never deletes)
curl -s https://singenote.pro/api/notes/<id>
# → 200 {"exists":true,"hasPassphrase":false,"expiresAt":"…"}  ·  404 when gone

# open & burn (returns ciphertext once, then deletes)
curl -s -X POST https://singenote.pro/api/notes/<id>/open \
  -H 'Content-Type: application/json' -d '{}'
# → 200 {"v":1,"ct":"…","iv":"…","salt":null}  ·  410 {"error":"gone"}

Errors are always {"error":"<code>","message":"…"}. Wrong passphrase: 403 bad_passphrase with remaining. Locked: 429 locked with retryAfterSec. Live counters: GET /api/stats (a server counter, not an on-chain figure).

Privacy

Holder verification

After launch, the Holder tier panel asks your wallet to sign a short plain-text message (EIP-191 personal_sign). It is not a transaction, costs no gas and approves nothing. The server recovers your address, reads your token balance on Robinhood Chain with balanceOf, and if you hold at least 500,000 tokens returns a session token valid for one hour.

Your address is never written to the database and is never stored alongside notes. Smart-contract wallets (EIP-1271) aren't supported in v1.

Holding unlocks bigger and longer notes. No staking, no rewards, no promises.

Token

Contract: contract address posted at launch

The token trades on Pons on Robinhood Chain. The only on-chain feature in SINGENOTE is the holder balance check above; notes are never stored on-chain.

Changelog