Status of This Memo
This document is not an Internet Standards Track specification; it is published for informational purposes, and for the purposes of the authors, who needed it.
This document is a product of the Network Working Group, which is two people and one couch, one of whom is standing. It has not been reviewed by the Internet Engineering Steering Group, which has not been asked and has not been told.
Information about the current status of this document, any errata, and how to provide feedback on it may be obtained by opening it again later and seeing whether it changed. It is inappropriate to cite this document other than as “work in progress”, which it will be, forever.
Copyright Notice
Copyright (c) 2026 the persons identified as the document authors, neither of whom has read the license either. This document and the code it describes are released under the MIT license, which is the one thing in here nobody argued about.
1.Introduction
Every other cloud remembers. It remembers what you wrote, to whom, when, from where, on what device, at what battery level, and, after a sufficiently long engagement with the ranking team, why. Big Social calls this memory a product. The industry calls it retention. This document calls it a liability with a logo.
PCTP is the other thing. It moves a message from a sender to a recipient such that (a) the message can be read exactly once, (b) no server involved in the exchange ever receives the message or its ciphertext, and (c) after the read, nothing remains anywhere under the operator’s control from which the message could be recovered: not by the operator, not by a subpoena, not by a sufficiently motivated intern.
It does this without accounts, without cookies, and without a database table that could hold a message, because there is no such table. The security argument does not rest on a promise to delete things. It rests on never having been handed them.
The feed ends. This is also a design goal.
Everything else in this document (the unit of time, the pandas, the state machine for unfinished projects) is required for a complete understanding of the protocol, in the sense that the author required it.
1.1Requirements Language
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
This document additionally defines the following key words, which are to be interpreted as described here and nowhere else:
- EVENTUALLY
- The requirement will be satisfied in a future release. No such release is scheduled. See Section 9.
- UNTIL HOT
- A duration bounded by an observable outcome rather than a clock. Equivalent to “as long as it takes”, except that the implementer checks.
- LOW-KEY
- The requirement is binding, but the author would prefer not to make a thing of it.
Finally, and applying to every conforming implementation of every part of this document:
An implementation MUST NOT be finished.
The rationale is in Section 9.3. It is shorter than you would expect and more binding than you would like.
1.2Terminology
- Pipe
- Any transport that moves bytes and is not asked to remember them. In the reference implementation, HTTPS.
- Cloud
- Storage that forgets. A cloud MUST expire everything it holds and MUST NOT hold anything that could be turned back into a message.
- Note
- The message. At most 4096 octets of UTF-8.
- Link
- The credential. Whoever holds it can read the note, once. There is no other credential.
- Fragment
- The part of the link after
#, as defined in Section 3.5 of [RFC3986]. The part that stays home. - Burn
- The single, irreversible release of the server share.
- Operator
- The person running the cloud. At time of writing, on a couch.
- Corpo
- An operator whose business model is Section 11 of this document, inverted.
- The Net
- The Internet, as described by someone wearing sunglasses indoors.
- Feed
- A list of posts. Elsewhere, endless by design. Here, ten posts long, by count.
- JD
- The unit of time. See Section 3.
2.Protocol Overview
PCTP has two moving parts and one part that refuses to move. This is the only section of the document that fits on a napkin, and Figure 1 is the napkin.
+-------------+ +---------------+
| sender | POST /api/create {ttl} ---> | server |
| browser | <--- {noteId, serverShare} | (writes |
+------+------+ | nothing) |
| +---------------+
| outer pipe: chat, mail, a napkin
|
| https://bongsmoke.cloud/n/<noteId>#1.<share>.<iv>.<ct>
| \___________________/
v fragment: never sent
+-------------+ +---------------+
| recipient | POST /api/reveal {noteId} ---> | server |
| browser | <--- {serverShare}, once | |
+-------------+ +-------+-------+
|
+-------v-------+
| cloud |
| SET .. NX EX |
+---------------+The sender’s browser asks the server for a note identifier. The server invents one, computes its half of the key from it, returns both, and writes nothing down. The sender’s browser generates the other half, encrypts, throws the server’s half away, and produces a link.
The link travels by whatever pipe the sender prefers: a chat app, an email, a napkin. PCTP has no opinion about that pipe, except that it is probably logging (Section 12).
The recipient opens the link. Nothing happens. They press a button. Their browser sends the identifier, and only the identifier; the server marks it burned and releases its half of the key, once. The browser reassembles the key and decrypts. At the end of the exchange the server has seen an identifier twice, produced the same 32-octet value twice, and never seen a message.
2.1Pipes
A pipe is transport. It MUST be TLS. It SHOULD be boring. The reference implementation’s pipes are two HTTPS endpoints, POST /api/create and POST /api/reveal. Each accepts a JSON body of at most 2048 octets and answers with Cache-Control: no-store, Referrer-Policy: no-referrer, and nothing a cache could keep.
PCTP distinguishes two classes of pipe:
- Inner pipes, operated by the protocol, which carry note identifiers and server shares and nothing else.
- Outer pipes, chosen by the sender, which carry the link.
PCTP secures the inner pipes. It cannot secure the outer pipes, and Section 12 is candid about what that costs. Pipes SHOULD be cleaned periodically; see status code 555 in Section 10.
2.2Clouds
A cloud is storage that forgets. Formally, a PCTP cloud is a key-value store in which every entry is of fixed size, has a time-to-live no longer than the longest note lifetime, and is keyed by a value from which nothing can be recovered.
The reference cloud is Redis, spoken to over HTTP. It holds exactly two kinds of entry, enumerated in Section 11.2. Neither contains a note, a ciphertext, a key share, or an IP address.
Other clouds remember. That is the whole difference, and it is not a small one.
3.Units of Measure: The JD
3.1Definition
Networking has long lacked a unit of latency a person can feel. Milliseconds are too small to experience and too large to brag about. PCTP therefore defines the JD.
One JD is the time required to microwave one Jimmy Dean Chocolate Chip Pancake Wrapped Sausage On A Stick.
1 JD = 90 s (or UNTIL HOT) 1 mJD = 90 ms 1 uJD = 90 us
The canonical value is fixed by this document. Implementations MUST NOT adjust it for microwave wattage, altitude, or whether the stick was frozen. The variance is real, and it is the reason the qualifier exists.
An operator who has stepped away to prepare one SHOULD answer 427 Snack Required, with Retry-After: 90 (Section 10).
The JD is a sincere homage. It was chosen because it is, to within the tolerance of a household appliance, the exact length of a context switch: long enough to lose the thread, short enough to insist you haven’t.
Implementations reporting latency SHOULD quote percentiles in JD, for example p99: 0.0004 JD. That is 36 milliseconds. It sounds like a lot less.
3.2Conversion Table
Every row below is a duration of record: one this site or the web platform actually uses, or one somebody actually measured. Only the unit is unusual.
| Quantity | Seconds | JD | Source |
|---|---|---|---|
| Nested setTimeout clamp | 0.004 | 0.00004444 JD | HTML Living Standard, timers |
| One frame at 60 Hz (one bamboo break) | 0.01667 | 0.0001852 JD | Section 7.3 |
| One second (legacy SI unit) | 1 | 0.01111 JD | BIPM, reluctantly |
| Clock-skew tolerance on note expiry | 300 | 3.33 JD | src/lib/limits.ts |
| Rate-limit window, per endpoint | 600 | 6.67 JD | src/lib/limits.ts |
| Shortest note lifetime (1h) | 3,600 | 40 JD | Section 6.1 |
| Ping by pigeon, mean round trip | 5,223 | 58.03 JD | [RFC1149]; Bergen, 2001 |
| Default note lifetime (1d) | 86,400 | 960 JD | Section 6.1 |
| Longest note lifetime (7d) | 604,800 | 6,720 JD | Section 6.1 |
| Light time, Sun to Neptune (mean), 30.07 AU | 15,005 | 166.72 JD | Section 11.4 |
The pigeon row is the mean of the four packets that came back. The other five are not in the table, or anywhere.
The longest note lifetime, 6,720 JD, is also the longest any entry lives in the cloud. The entire memory of the service, at its fullest, lasts one week of breakfasts, if breakfast is continuous.
The Neptune row is measured from the Sun, at Neptune’s mean distance of 30.07 AU, because that number holds still. The figure from Earth, tonight, does not; the homepage computes it live from where the planets actually are.
3.3Latency Budgets
This document does not publish measured latencies, because measuring them would require collecting them. It publishes budgets instead.
- A MINT round trip SHOULD complete within 0.01 JD (900 ms).
- A BURN round trip SHOULD complete within 0.01 JD. It is one Redis round trip, made from a function pinned to the database’s region, which matters more for a single round trip than being near the caller.
- Key derivation without a passphrase is one HKDF invocation and SHOULD be imperceptible.
- Key derivation with a passphrase runs 600,000 iterations of PBKDF2-SHA256 and is perceptible on purpose. It is the only part of PCTP that is slow deliberately.
- The smoke MAY take as long as it likes. It is not on the critical path, only the emotional one.
4.Message Formats
The grammars in this section use ABNF [RFC5234]. They are transcribed from the reference implementation, not the other way around, which is the order in which grammars are usually written and the opposite of the order in which they are usually admitted to have been.
4.1The Note Identifier
note-id = random "." expiry random = 22base64url-char ; 128 random bits expiry = 6*9base36-char ; Unix seconds, base 36 base64url-char = ALPHA / DIGIT / "-" / "_" base36-char = DIGIT / %x61-7A ; 0-9 a-z
The identifier is 128 random bits followed by the moment the note dies, in the clear. It is generated by the server and MUST NOT be accepted from a client. This is load-bearing: if a client could name its own identifier, it could request the server share for somebody else’s note through MINT, without tripping that note’s burn marker.
The expiry is readable by anyone and trusted by no one. It is an input to the HMAC that produces the server share (Section 5), so editing it yields a different identifier, a different share, and a key that decrypts nothing. Tampering is not prevented. It is simply pointless.
A server MUST reject, before touching storage, any identifier whose expiry has passed or lies further out than the longest lifetime offered (7 days) plus 300 seconds of clock-skew tolerance. Such identifiers are answered with 410, exactly as notes that never existed are, because the server keeps no record that could tell them apart.
4.2The Fragment
fragment = plain / guarded plain = "1" "." share "." iv "." ciphertext guarded = "1p" "." share "." iv "." ciphertext "." kdf-salt share = base64url ; 32 octets: the client share iv = base64url ; 12 octets ciphertext = base64url ; padded payload + 16-octet GCM tag kdf-salt = base64url ; 16 octets, passphrase notes only
https://bongsmoke.cloud/n/Q2x1ZW...Rk1h.t1f3k2q#1.<share>.<iv>.<ct>
\_____________________/\______________________/ \_________________/
origin path + note-id fragment
sent: DNS, TLS sent: request line sent: to nobody,
everA browser does not transmit the fragment of a URI [RFC3986]. It is not in the request line. It is not in the Referer header, from which browsers strip fragments and which PCTP deployments suppress entirely with Referrer-Policy: no-referrer. It does not appear in the server’s logs, or in the logs of any proxy in between, because none of them ever receive it.
Everything a reader needs, except one value, therefore rides in the one place on the Net that is structurally private. PCTP did not invent this property. It merely noticed that almost nobody was using it for anything except scrolling to headings, such as the ones in this document.
The finished link MUST NOT exceed 8000 characters, beyond which mail clients and proxies begin truncating URLs in ways that are difficult to debug and impossible to forgive.
4.3Payload Framing and Padding
0 1 5 N bucket +---------+----------------+--------------------+------------+ | flags | body length | body | 0x00 ... | | bit 0: | uint32, | raw DEFLATE if | padding | | DEFLATE | big-endian | that helped; | | | | | else UTF-8 | | +---------+----------------+--------------------+------------+ buckets (octets): 128 256 512 1024 2048 3072 4096 5120
Before encryption the plaintext is compressed with raw DEFLATE if, and only if, that makes it smaller. Prose roughly halves. A pasted API key does not, and is sent as written. A five-octet header records which happened and how long the body is.
The result is zero-padded up to the smallest of eight size buckets. Ciphertext length is the one property of a note that an observer of the link can measure without breaking anything, and unpadded length is loud: it tells “the wifi password” from “the resignation letter” at a glance. Buckets reduce that to a coarse hint.
Plaintext is capped at 4096 octets. The arithmetic: 4096 raw octets pad into the 5120-octet bucket at worst; add 32 (share) + 12 (IV) + 16 (tag) + 16 (salt) for 5196 octets, about 6928 base64url characters, which with origin, path and identifier lands near 7 KB, under the 8000-character ceiling. The budget was worked backwards from the ceiling, which is the only direction in which budgets are ever worked.
5.Key Schedule
The key that opens a note is never stored anywhere, in one piece, by anyone. It is derived at read time from shares that live apart.
clientShare 32 octets getRandomValues; fragment only --+
serverShare 32 octets HMAC-SHA256(SERVER_SECRET, id) --+
passphraseShare 32 octets PBKDF2-SHA256, 600,000 iter. --+
(OPTIONAL) |
IKM, concatenated, in order |
v
+-------------------------------------------------------------+
| HKDF-SHA256( salt = note-id, |
| info = "up-in-smoke/v1/aes-256-gcm" ) |
+------------------------------+------------------------------+
|
v
AES-256-GCM key --> decrypt( iv, ct, aad = note-id )- clientShare
- 32 octets from
crypto.getRandomValues, generated in the sender’s browser. Lives only in the fragment. - serverShare
HMAC-SHA256(SERVER_SECRET, noteId)[RFC2104]. Never stored, because it can be recomputed at will. Released by the server once, at BURN.- passphraseShare
- OPTIONAL. PBKDF2-SHA256 [RFC8018] over the NFKC-normalized passphrase, 600,000 iterations, with a random 16-octet salt carried in the fragment.
The shares are concatenated in that order and used as input keying material for HKDF-SHA256 [RFC5869], with the note identifier as salt and up-in-smoke/v1/aes-256-gcm as info. The output is a 256-bit AES-GCM key [SP800-38D]. Each note gets a fresh 96-bit IV and a 128-bit tag. The note identifier is bound in as additional authenticated data, so a ciphertext lifted from one link and replayed under another identifier fails authentication rather than decrypting into garbage.
The info string says up-in-smoke. That was the project’s name before it was this one. Changing the string would change every key and break every outstanding link, so it stays: a small fossil in the key schedule, and a reminder that the most permanent thing in any system is a string somebody meant to change later.
Neither share alone opens anything. The server holds one and cannot read the note. The link holds the other and cannot be read without asking the server. The asking succeeds once.
6.Transport Operations
PCTP defines two operations. There is no UPDATE, no LIST, and no DELETE, because there is nothing to update, list, or delete. The reference implementation of both is at /burn.
6.1MINT
POST /api/create {"ttl": "1h" | "1d" | "7d"}
200 {"noteId": "...", "serverShare": "..."}
400 {"error": "invalid_ttl"}
429 Retry-After: n {"error": "rate_limited"}MINT is stateless. It MUST NOT write to the cloud. It mints an identifier, derives the matching share, and forgets both. Because nothing is stored, flooding MINT costs the operator one HMAC per request and nothing that scales with a bill: no rows to fill, no cleanup job.
MINT is rate limited to 60 requests per address per ten minutes. If the cloud is unreachable, the rate limiter cannot count, and MINT proceeds unmetered rather than failing the sender, since it writes nothing either way.
The sender’s browser discards serverShare as soon as the note is encrypted. It is not in the link. It is not anywhere.
6.2BURN
reader /api/reveal cloud
| | |
| [a human presses the button] | |
|--- {"noteId": ...} ---------->| |
| |-- parse id, check expiry |
| |-- INCR rl:reveal:<h(ip)> ->|
| |-- SET b:<h(id)> 1 NX EX -->|
| |<-- OK | nil ---------------|
|<-- 200 {serverShare} ---------| OK: derive, release |
|<-- 410 {"error":"gone"} ------| nil: already burned |
|<-- 503 {"error":...} ---------| no answer: fail closed |
| | |
|-- HKDF, AES-GCM decrypt, inflate
|-- history.replaceState(path)The request body carries the note identifier and nothing else. The server, in this order:
- parses the identifier and checks its expiry (Section 4.1), answering 410 if either fails;
- counts the request against the caller’s rate limit;
- performs one atomic
SET b:<key> 1 NX EX <seconds until expiry>, where<key>is a truncated HMAC of the identifier under the server secret, not the identifier itself; - if and only if the
SETreturnedOK, derives and returns the server share.
The ordering is normative. The marker is claimed before the share is derived, and the share is released only on a successful claim. SET … NX is a single atomic operation: the first caller creates the key and receives OK; every other caller, including a dozen arriving in the same millisecond from different regions, receives nil and a 410. There is no read-then-write window for a racer to slip through.
If the cloud cannot be reached, BURN answers 503 and releases nothing. A note that cannot be marked burned MUST NOT be opened. The failure mode of PCTP is “temporarily unavailable”, never “quietly readable twice”.
This is the one place in the protocol where the Couch Mode instinct (do nothing) and the security requirement (do nothing) coincide exactly.
6.3The Preview Bot Problem
Every messaging app of consequence fetches a link the moment it is pasted, to draw a preview card. A protocol that burned on GET would be burned by the chat app before the recipient ever saw the message.
PCTP therefore requires that opening a link fetch nothing. The reader’s page loads, parses the fragment locally, and waits. BURN is issued only when a human presses a button. A preview bot receives a page that says a note is waiting, which is true, and leaves with nothing, which is also true.
Every note link resolves to one route. The identifier is read out of the path by the browser, so it is never a route parameter and never reaches a server-side render. Note pages are served with Cache-Control: no-store and X-Robots-Tag: noindex, nofollow, noarchive, nosnippet, so neither caches nor crawlers keep a page that was never worth keeping.
6.4Fragment Hygiene
After a successful decrypt, the reader’s browser replaces the current history entry with the bare path, which removes the fragment from the address bar and from that tab’s history. This reaches exactly one tab. It cannot reach the sender’s history, the recipient’s other devices, or the messaging app that carried the link (Section 12).
Every page but one carries a Content-Security-Policy with a fresh nonce per request. On the two pages that ever hold a secret, /burn and the note page, it also says connect-src 'self' and nothing loads from any other origin: an injected script would find it has nowhere to send a decrypted note. Every other page names one more origin, the operator’s own visit counter (Section 11.3). No page allows 'unsafe-eval' or 'unsafe-inline' for scripts, and an injected script would be advised to reconsider its career.
The one is /neptune, the deep end. It is excluded from the nonce middleware and served as static files under a fixed policy that adds exactly one source expression, 'wasm-unsafe-eval', so its engine can compile WebAssembly. It keeps frame-ancestors 'none' and the visit counter, still has no 'unsafe-eval', and never holds note material: no note, no fragment, no key.
The nonce costs something: pages render on demand rather than from a CDN cache. In exchange, nothing on this site can call anywhere its policy does not name, and no policy here names more than two places: this site and the visit counter. On a note page it names one. This document ships no script of its own and was delivered to you under exactly that policy.
7.Scheduling: JavaScript Pandamonium
PCTP is implemented in JavaScript, and JavaScript is scheduled by an event loop. Field observation has established that the event loop contains pandas. This section specifies their behavior, which is identical to the behavior given in the event loop section of the HTML Living Standard [HTML], because it is that behavior.
The field guide to this section lives at /pandamonium. It is a working model of the loop: guess the order the code logs, step through it one panda at a time, then run the same code on your own browser’s event loop and compare. They match. The field guide is normative in the sense that your browser is.
+----------------+ +------------------+ +----------------+
| GIANT PANDA | | RED PANDAS | | BAMBOO BREAK |
| one task |---->| every micro- |---->| if a frame is |
| (a click, a | | task, and every | | due: rAF, |
| response, a | | one they queue, | | style, |
| timer) | | until empty | | layout, paint |
+----------------+ +------------------+ +-------+--------+
^ |
+-----------------------------------------------+7.1Red Pandas
Red pandas are microtasks: promise continuations, queueMicrotask, mutation observer callbacks. They are small, they are fast, and they cut the line. When a giant panda leaves, every queued red panda runs, including any red pandas those red pandas enqueue, before anything else is allowed to happen.
Decrypting a note is a relay of red pandas: import the key, derive, decrypt, inflate, each step an await, each resumption a red panda handing the baton to the next.
A red panda that endlessly enqueues another red panda starves every giant panda and every bamboo break behind it. The page stops painting and stops responding and appears, from the outside, exactly like the author on a weekday afternoon. Implementations MUST NOT build unbounded red panda chains.
7.2Giant Pandas
Giant pandas are macrotasks. The standard’s own word is simply “task”: a click, a network response, a timer. They go one at a time, and only one giant panda is ever in the enclosure.
setTimeout(fn, 0) is a giant panda, not a red panda, and waits its turn. Once timers nest more than five deep, the standard clamps them to at least 4 ms, or 0.00004444 JD, a figure every JavaScript developer learns at the moment they least want to.
setTimeout with a string argument is also a giant panda, but it is a giant panda that calls eval, and the Content-Security-Policy of every page on this site turns it away at the gate. So does new Function. This includes /neptune: 'wasm-unsafe-eval' admits WebAssembly and nothing that eats strings.
7.3The Bamboo Break
Between giant pandas, when the browser decides a frame is due, everyone stops for bamboo: animation frame callbacks run, style and layout are worked out, and the page is painted. At 60 Hz a bamboo break falls due about every 16.7 ms, or 0.0001852 JD.
The smoke that consumes a revealed note lives entirely inside bamboo breaks. Its particle buffer is uploaded to the GPU once; after that, every frame, the CPU writes one float (progress) and the vertex shader works out the rest. It is the lightest bamboo there is, and it is why the smoke can rise without making any panda wait.
7.4Trash Pandas
Trash pandas were not in the original design. They were not in any design. They are not giant pandas, they are not red pandas, and they do not wait for the bamboo break. Nobody scheduled them. They slipped in under the radar, and they eat the garbage.
Trash pandas are the garbage collector. Garbage is any object that can no longer be reached from a root: the global object, the stack, and the callbacks held by every panda still waiting in a queue. What nothing can reach, the trash pandas may eat. What something can still reach, they leave alone, however dead it looks (Section 7.5).
Neither ECMAScript [ECMA262] nor the HTML Living Standard [HTML] says when trash pandas run, or whether they run at all. They are in no queue Section 7 defines. Engines feed them a little at a time, on other threads, or in the middle of whatever else was happening. The authors are not sure why they are here, and the specifications are not sure either, on purpose.
A program can watch the bins in exactly two ways. A WeakRef answers undefined from deref() once its target has been eaten; a target it has just returned is left alone at least until the red pandas queued behind the current code have all run, because HTML clears the kept objects at the end of each microtask checkpoint. A FinalizationRegistry schedules a cleanup callback for afterward, and ECMAScript permits an implementation never to call it.
Implementations MUST NOT depend on the arrival of trash pandas.
In particular, nothing that matters (a lock, a connection, a secret) may be released from a cleanup callback, since the callback may never come. PCTP does not ask them to: a note is burned by the SET of Section 6.2, not by garbage collection. The one thing the trash pandas do own is the decrypted note in the reader’s tab, which stays in memory until they get to it. JavaScript has no way to wipe a string. They will get to it when they get to it.
The field guide at /pandamonium shows them at work, alongside the pandas, in the reader’s own browser.
7.5Possums
The trash pandas brought possums. A possum is an object the program is finished with that is still referenced from somewhere, so it is still reachable, so the trash pandas, who eat only what cannot be reached, walk right past it. This is a memory leak. It looks dead. It is holding on to everything it points at.
A possum that is still referenced is not dead. It is playing dead.
Possums are most often found:
- on long-lived targets: a listener added to
windowordocumentfor a component that has since gone, still holding its closure, and through the closure everything the closure can see; - in timers: a
setIntervalnobody cleared keeps its callback, and everything that callback captured, for the life of the page; - in closures kept by something else: a callback handed to an observer, a cache or an event bus captures its whole scope, including the large object nobody meant it to keep;
- in global caches: a
Mapor array at module scope that is written on every request and read on none, gaining one possum per request; - under the porch: detached DOM nodes, removed from the document but still held in a variable, keeping their whole subtree with them.
The remedy is to stop referencing them; the trash pandas do the rest. Remove a listener when its owner goes, with removeEventListener and the same function and capture flag it was added with, or register listeners with an AbortSignal and call abort() on its AbortController once, which removes all of them. Clear intervals. Bound caches, or key them with a WeakMap, whose entries do not keep their keys alive. Use WeakRef only where “it is gone now” is an acceptable answer, since Section 7.4 makes no promise about when that answer will change.
Implementations SHOULD NOT keep possums. A request for one MAY be answered 432 Playing Dead (Section 10). To find one, take a heap snapshot in the browser’s developer tools and follow its retainers back to whoever is still holding on. Detached DOM nodes are labeled as such.
8.Operating Modes
A PCTP implementation runs in one of two operating modes. They are not preferences. They are weather.
| Characteristic | Couch Mode | Sativa Mode |
|---|---|---|
| Posture | Horizontal | Technically standing |
| Browser tabs | 1 | 47 |
| Accent | Warm ember | Acid |
| Response to a new idea | Noted | New repository within 0.1 JD |
| Characteristic status | 437 Couch Lock | 287 Eighty-Seven Percent |
| Commit message | wip | 2,000 words and a diagram |
| Ships | No | No, but faster |
8.1Mode Negotiation
Mode is chosen by the reader, with the switch at the top of every page, and is remembered in the reader’s own browser under a single localStorage key, bongsmoke:mode. It is not a cookie. It is never transmitted. Returning to Sativa deletes the key outright, so the default state is the state in which nothing is stored at all. Sativa Mode is the default. Couch Mode can be requested, and is then honored without comment.
Independently of mode, a user agent that advertises prefers-reduced-motion: reduce MUST NOT be shown moving smoke. The ambient smoke does not start, and a revealed note fades out plainly instead of rising. A user agent without WebGL2 receives the same treatment involuntarily, which is how most people arrive at the couch.
Every path reaches the same outcome: the note is read, then gone. Mode affects how a note leaves, never whether.
8.2Couch Lock
Couch Lock (status 437) is the condition in which an implementation agrees with a request in principle and is not getting up. It differs from a crash in that the implementation is fine, and from a hang in that it is aware of the situation.
An implementation in Couch Lock SHOULD answer aggressive retries with 420 Enhance Your Calm, in keeping with historical practice (Section 10). The reference implementation answers 429 instead, with a Retry-After header, because it is a real server with real clients and it is not about to be cute about rate limiting.
9.Project Lifecycle: The Graveyard
This section specifies the lifecycle of a project. It is included because PCTP is a project, the author has others, and a protocol that ignored the conditions of its own development would be dishonest in a way this document otherwise works hard to avoid. The census of the departed is kept at /graveyard.
9.1States and Transitions
+------+
+--->| IDEA |
| +--+---+
| | CHECKOUT
| v
| +------------------+
| | DOMAIN_PURCHASED |
| +--+---------------+
| | DOCUMENT_THE_DREAM
| v
| +----------------+
| | README_WRITTEN |
| +--+-------------+
| | MOMENTUM
| v
| +------------+ NEW_FRAMEWORK_RELEASED +---------+
| | 87_PERCENT |--------------------------->| REWRITE |
| | |<---------------------------| |
| +--+------+--+ MOMENTUM +---------+
| | ^
| | +-------------------+
| | SIX_MONTHS_PASS | SHOWER_THOUGHT
| v |
| +---------+ |
| | DORMANT |-------------------+
| +--+------+
| | DOMAIN_RENEWAL_EMAIL
| v
| +-----------+
| | GRAVEYARD |
| +--+--------+
| | SHOWER_THOUGHT
+-------+
+----------+
| FINISHED | no inbound transitions; see Section 9.3
+----------+| From | Event | To | Complete |
|---|---|---|---|
| IDEA | CHECKOUT | DOMAIN_PURCHASED | 40% |
| DOMAIN_PURCHASED | DOCUMENT_THE_DREAM | README_WRITTEN | 80% |
| README_WRITTEN | MOMENTUM | 87_PERCENT | 87% |
| 87_PERCENT | NEW_FRAMEWORK_RELEASED | REWRITE | 12% |
| REWRITE | MOMENTUM | 87_PERCENT | 87% |
| 87_PERCENT | SIX_MONTHS_PASS | DORMANT | 87% |
| DORMANT | SHOWER_THOUGHT | 87_PERCENT | 87% |
| DORMANT | DOMAIN_RENEWAL_EMAIL | GRAVEYARD | 87% |
| GRAVEYARD | SHOWER_THOUGHT | IDEA | 0% |
| (any) | SHIP | (unchanged) | n/a |
SHIP is a valid event in every state. It does not change the state. FINISHED is a defined state with no inbound transition. The reference implementation’s test suite verifies, on every run, that FINISHED is unreachable from every other state. It passes.
Note that completion is not monotonic. A rewrite resets progress to 12% and then, via MOMENTUM, returns it to a number that has in every observed case been 87 again (status 287). As [RFC1925] observes in truth (11), every old idea will be proposed again with a different name and a different presentation, regardless of whether it works.
9.2The Half-Life
A project’s half-life is the time it takes for half of its remaining motivation to decay. It is shorter than the domain registration and longer than the free trial of whatever the project was going to be hosted on.
m(t) = 2^(-t / t_half) motivation remaining p(t) = 0.87 * (1 - m(t)) completion lim p(t) = 0.87 as t -> infinity
Motivation decays. Completion does not; it rises quickly toward 87% and approaches it forever. The gap between the two curves is where the Graveyard gets its residents. The full model, with its coefficients fitted to a sample of one author, is at /graveyard. The ceiling is not a parameter.
9.3On Shipping
Shipping is a state of mind (status 299). An implementation MUST NOT be finished.
This requirement is not aspirational. A finished implementation has, by definition, stopped responding to the world, and the world is not finished either. Dependencies move. Browsers change what they will allow. Somebody finds a better way to be wrong. Software that has stopped changing has not been completed; it has been abandoned in a completed position.
PCTP itself has been 87% done since 2019, a date that predates the protocol and has been kept for continuity. Note the corollary: bongsmoke.cloud is live, handles real notes, and burns them correctly. It is deployed on every push. It is not finished. All three statements are true at once, and Section 9.1 explains why that is not a contradiction but a state machine.
Extensions for being done are specified separately in [RFC-4:21].
10.Status Code Registry
This section establishes the PCTP Status Code Registry. Each entry is labeled with its provenance, because a registry is only useful if a reader can tell which lines are load-bearing:
- emitted here
- Returned by the reference implementation, today, from
/api/createor/api/reveal. - historical
- Real, with a paper trail somewhere else.
- PCTP-local
- Defined here. Registered with nobody.
- 287Eighty-Seven PercentPCTP-local
The request succeeded in every respect except completion. The body is the part that works. The rest is on a branch.
Reference: Section 9
- 299Shipping Is A State Of MindPCTP-local
The server considers the resource shipped. No deployment has occurred. Clients MUST NOT ask follow-up questions.
Reference: Section 9.3
- 400Bad Requestemitted here
Returned with {"error":"invalid_ttl"} when a note lifetime is anything other than 1h, 1d or 7d.
Reference: RFC 9110; POST /api/create
- 410Goneemitted here
The note was already burned, has expired, or never existed. These are one answer on purpose: the server keeps no record that could tell them apart.
Reference: RFC 9110; POST /api/reveal
- 418I'm a teapothistorical
Defined by HTCPCP/1.0 on 1 April 1998 for a teapot asked to brew coffee; extended to tea by HTCPCP-TEA in 2014; reserved as "(Unused)" by RFC 9110 so nobody can ever assign it to anything sensible.
Reference: RFC 2324, RFC 7168, RFC 9110
- 420Enhance Your Calmhistorical
Returned by Twitter's version 1 Search and Trends APIs to clients being rate limited. Version 1.1 moved to 429. PCTP retains 420 as the preferred code for Couch Mode back-pressure (section 8).
Reference: Twitter Search and Trends API v1
- 427Snack RequiredPCTP-local
The operator is away from the keyboard. The response MUST carry Retry-After: 90, which is one JD.
Reference: Section 3
- 429Too Many Requestsemitted here
More than 60 requests to one endpoint from one address in a ten-minute window. Carries Retry-After. The address is counted under a keyed hash and never stored.
Reference: RFC 6585; both endpoints
- 432Playing DeadPCTP-local
The resource looks gone and is still referenced, so it is not. Distinct from 410, which PCTP returns for things that are gone, or never were, and cannot tell which. A 432 resource is neither. Retrying with kill -9 is not supported.
Reference: Section 7.5
- 437Couch LockPCTP-local
The server understood the request, agrees with it in principle, and is not getting up.
Reference: Section 8.2
- 451Unavailable For Legal Reasonshistorical
Named for Fahrenheit 451. Listed for completeness and because this document is about things that burn.
Reference: RFC 7725
- 503Service Unavailableemitted here
The burn marker could not be written, so the share is not released. PCTP fails closed: temporarily unavailable, never quietly readable twice.
Reference: RFC 9110; POST /api/reveal
- 555Resin BuildupPCTP-local
Pipe throughput has degraded gradually and nobody noticed until now. Remedy is out of scope for this document.
Reference: Section 2.1
Code 418 was defined by [RFC2324] for a teapot that has been asked to brew coffee, and extended to tea by [RFC7168]. Both were published on the first of April. When HTTP’s semantics were consolidated in [RFC9110], 418 was marked “(Unused)” and reserved, so that no future specification could assign it to anything serious. It shares that distinction with 306, which is reserved for the less charming reason that it once meant something and then stopped.
Code 420 was never registered. Twitter’s version 1 Search and Trends APIs returned it, with the phrase Enhance Your Calm, to clients that were being rate limited; version 1.1 moved to the registered 429 [RFC6585]. PCTP retains 420 for the purpose it was evidently meant for.
11.Privacy Considerations
11.1Information Anarchy
No gods, no masters, no ad trackers.
The operating principle of PCTP’s privacy model is that data never collected can never leak. It cannot be sold, subpoenaed, breached, “anonymized”, used to train anything, or found in a public bucket by a researcher with a blog. PCTP achieves privacy the way a cloud achieves forgetting: by not having the thing.
Concretely, and verifiably by anyone with a browser’s developer tools, bongsmoke.cloud:
- sets zero cookies. Not “only essential cookies”. Zero.
- has no accounts, no sign-in, and no user table, and therefore nobody to sign in as.
- runs no ad tracker, no tag manager, no pixel, no session replay, and no A/B test. It counts visits, with a counter of its own (Section 11.3).
- loads nothing from any origin but its own and its visit counter’s: no fonts from elsewhere, no script CDNs, no remote images. On the two pages that hold a secret, not even the counter. The Content-Security-Policy says so on every page, the deep end included, and the browser enforces the policy even on a day the operator forgets.
- has a feed. It is ten posts long, sorted by nothing, and it ends. There is no page two, no cursor, and no “load more”; the array simply stops.
Big Social measures success in time on site. The visit counter measures it too, and PCTP regards it as latency.
11.2What Is Stored
The cloud holds two kinds of entry. Both are fixed-size. Both expire on their own.
- Burn markers
- Value
1, underb:followed by the first 16 octets ofHMAC(SERVER_SECRET, "burn" ‖ 0x00 ‖ noteId), base64url. Lives until the note would have expired anyway: at most 7 days, or 6,720 JD. - Rate-limit counters
- One integer per endpoint per ten-minute window, keyed by an HMAC of the caller’s address under the same secret, expiring ten seconds after the window closes. No raw address is written.
A complete dump of the cloud reveals how many notes were opened recently, when they would have expired, and how busy some unidentifiable addresses were in the last ten minutes. It reveals nothing else, because it contains nothing else. Creating a note writes nothing at all.
11.3What Is Seen Anyway
Information Anarchy is a principle, not a force field. The hosting platform records HTTP requests, as every host does. That record includes the path of a note link, which contains the note identifier, along with the time and the requesting address. The fragment is never in it.
PCTP does not log request bodies or identifiers itself, but it cannot un-log what the platform logs. The consequence is a privacy leak and not a confidentiality one: an identifier cannot decrypt anything. Someone with access to those logs could learn that a note was opened, when, and from where, and could burn a note before its recipient by replaying its identifier. The bar for that is already having access to the operator’s infrastructure logs.
The operator counts visits with its own privacy-friendly analytics. It sets no cookies and stores nothing on the reader’s device; the reader’s IP address is used only to make a daily anonymous count and is never stored. To opt out on this site, open any page with ?ov_ignore=1.
The counter lives at overlook.technology. It is told a page’s address, with the query cut down to utm_*, ref and source and the fragment dropped, plus the referrer, whether the device has a touchscreen, how long the page was in use and how far it was scrolled, and Web Vitals. From the request it works out country, region and city, and browser, operating system and device. It is not told which links a reader follows out. It never loads on /burn or a note page, so no note identifier and no fragment ever reaches it.
The Couch/Sativa preference (Section 8.1) is stored in the reader’s browser, not here, and is never sent anywhere. So is the counter’s opt-out, for a reader who sets one.
11.4The Consent Banner
Because PCTP sets no cookies, it has nothing to ask consent for. A consent banner on this site would be a dialog requesting permission to do nothing, with an “Accept all” button that accepts nothing and a “Manage preferences” panel containing no preferences.
Deleting it would have been the obvious move, and deleting things is hard (Section 9). The banner was instead placed in a heliocentric orbit at 30.07 AU, Neptune’s mean distance from the Sun, co-orbital with Neptune, where it remains fully compliant and entirely unread.
Sun Earth Neptune
@ o . . . . . . . . . . . . . . . . . O -[ We use cookies! ]
| | | (we don't)
| 1 AU 30.07 AU, mean
| |
|<------------------------------------------>|
from the Sun (mean): ~166.7 JD at c, one way
(the site, its contents, you)Light leaving the Sun takes about 166.7 JD (4.17 hours) to reach the banner, measured from the Sun (mean). A consent request sent from the banner to a reader on Earth takes roughly as long, and as long again for the refusal to get back; the exact figure from Earth, tonight, depends on where both planets are and is computed live on the homepage.
The banner can be visited at /neptune, the deep end of this site and the only page permitted to run WebAssembly (Section 6.4). It asks for nothing, stores nothing, and keeps no save file: its engine is configured with persistentPaths: [], so it opens no browser storage at all. Bring snacks.
12.Security Considerations
This section is normative, accurate, and the least funny part of this document, in that order. The full threat model is published at /security; what follows is a summary, and where the two disagree, that page wins. A security section that only lists strengths is marketing.
- The link is the credential.
- Anyone who sees it can read the note, once. The recipient then finds it burned, which is the only signal anyone gets. A burned note that its recipient never opened is evidence of interception and SHOULD be treated as such. Add a passphrase if the outer pipe is not one you trust.
- Links persist in places PCTP does not control.
- In the sender’s browser history and autocomplete. In the messaging app that carried it, its backups, its cloud sync, and its server-side message store. In the recipient’s history until a successful reveal clears it, which reaches one tab and no further.
- A reader can keep the note.
- Burning destroys the key, not the reader’s memory, screenshot, clipboard, or the text file they pasted it into. “Self-destructing” describes the service, not the recipient.
- Whoever controls this site can serve different code.
- All of the cryptography runs in JavaScript served by this site. A compromised deployment, a stolen hosting credential, or a malicious commit could serve a version that exfiltrates the fragment before decrypting it. This is the fundamental limit of browser-delivered cryptography, and no client-side design fixes it. What is done: no third-party scripts, and on the two pages that hold a secret, nothing from any other origin and a policy of
connect-src 'self'. The rest of the site also runs the operator’s visit counter, so there a reader trusts that deployment too. What is not done: there is no code signing and no reproducible build. You are trusting the deployment. - Metadata is not hidden.
- See Section 11.3. Length is bucketed (Section 4.3), not hidden: the link reveals which bucket a note fell into.
- The passphrase is PBKDF2, not Argon2id.
- Argon2id resists GPU cracking better. The audited implementations are WebAssembly, which would require
'wasm-unsafe-eval'in the script policy of every page that handles a note. PBKDF2-SHA256 at 600,000 iterations is OWASP’s current floor, and it is a third factor on top of a 256-bit share and a one-time server share, not the only thing between an attacker and the plaintext. - Rotating the server secret breaks every unread link.
- Silently. It also resets every burn marker, so notes already read become readable again if their link survives and has not expired. Rotate if the secret leaked. Otherwise, Couch Mode.
- The deep end has a different policy.
- /neptune is excluded from the per-request nonce and served static under its own fixed Content-Security-Policy, which adds
'wasm-unsafe-eval'so the engine can compile and adds nothing else a script could use: no'unsafe-eval', no'unsafe-inline'for scripts. It keepsframe-ancestors 'none', and its only other origin is the visit counter’s. It never holds note material (no note, no fragment, no key) and sets no persistent storage, so the weaker policy guards nothing worth taking. - Not covered.
- Traffic analysis, or a global adversary correlating timing. A compromised endpoint on either side. Denial of service beyond per-address rate limiting. Anyone who has the link and is willing to burn the note to read it.
Readers arriving from r/masterhacker will note that none of this requires a hoodie. Vulnerabilities SHOULD be reported through the contact named in /.well-known/security.txt, describing the class of problem rather than publishing a recipe. Details that should not sit in an issue tracker MAY travel as a burn note. The operator will read it before it burns. That is the only guarantee on offer, and it is the same one the protocol makes.
13.IANA Considerations
IANA is requested to do nothing, slowly.
In particular, IANA is requested not to register the status codes of Section 10, not to allocate a port for PCTP (it uses 443, like everything else that wants to get through a firewall), and not to create a registry of JD sub-units. Should IANA nevertheless feel compelled to act, it is requested to do so in Couch Mode.
This document has no IANA actions. It has several IANA inactions, enumerated above, and the authors are grateful for each of them.
14.References
14.1Normative References
- [COUCH]
- Lock, C., “On the Measurement of Time Using Breakfast”, Unpublished, perpetually.
- [JD]
- Jimmy Dean Chocolate Chip Pancake Wrapped Sausage On A Stick, one (1), microwaved UNTIL HOT.
- [RFC2104]
- Krawczyk, H., Bellare, M., and R. Canetti, “HMAC: Keyed-Hashing for Message Authentication”, RFC 2104, February 1997.<https://www.rfc-editor.org/info/rfc2104>
- [RFC2119]
- Bradner, S., “Key words for use in RFCs to Indicate Requirement Levels”, BCP 14, RFC 2119, March 1997.<https://www.rfc-editor.org/info/rfc2119>
- [RFC3986]
- Berners-Lee, T., Fielding, R., and L. Masinter, “Uniform Resource Identifier (URI): Generic Syntax”, STD 66, RFC 3986, January 2005.<https://www.rfc-editor.org/info/rfc3986>
- [RFC5234]
- Crocker, D., Ed. and P. Overell, “Augmented BNF for Syntax Specifications: ABNF”, STD 68, RFC 5234, January 2008.<https://www.rfc-editor.org/info/rfc5234>
- [RFC5869]
- Krawczyk, H. and P. Eronen, “HMAC-based Extract-and-Expand Key Derivation Function (HKDF)”, RFC 5869, May 2010.<https://www.rfc-editor.org/info/rfc5869>
- [RFC8018]
- Moriarty, K., Ed., Kaliski, B., and A. Rusch, “PKCS #5: Password-Based Cryptography Specification Version 2.1”, RFC 8018, January 2017.<https://www.rfc-editor.org/info/rfc8018>
- [RFC8174]
- Leiba, B., “Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words”, BCP 14, RFC 8174, May 2017.<https://www.rfc-editor.org/info/rfc8174>
- [SP800-38D]
- Dworkin, M., “Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC”, NIST Special Publication 800-38D, November 2007.
14.2Informative References
- [BURN]
- bongsmoke.cloud, “Burn a note”, the reference implementation of Sections 4 through 6. /burn
- [ECMA262]
- Ecma International, “ECMAScript Language Specification”, ECMA-262, sections “WeakRef Objects” and “FinalizationRegistry Objects”.<https://tc39.es/ecma262/>
- [GRAVEYARD]
- bongsmoke.cloud, “The Graveyard”, a census of the departed and the Half-Life model. /graveyard
- [HTML]
- WHATWG, “HTML Living Standard”, section “Event loops”.<https://html.spec.whatwg.org/multipage/webappapis.html#event-loops>
- [NEPTUNE]
- bongsmoke.cloud, “Neptune”, the deep end. Requires WebAssembly, 'wasm-unsafe-eval', and patience. /neptune
- [PANDAMONIUM]
- bongsmoke.cloud, “JavaScript Pandamonium”, the field guide to Section 7: a working model of the event loop, checked against the reader’s own. /pandamonium
- [RFC1149]
- Waitzman, D., “Standard for the transmission of IP datagrams on avian carriers”, RFC 1149, 1 April 1990. Implemented in Bergen, Norway, on 28 April 2001. Four of nine packets came back.<https://www.rfc-editor.org/info/rfc1149>
- [RFC1925]
- Callon, R., “The Twelve Networking Truths”, RFC 1925, 1 April 1996.<https://www.rfc-editor.org/info/rfc1925>
- [RFC2324]
- Masinter, L., “Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)”, RFC 2324, 1 April 1998.<https://www.rfc-editor.org/info/rfc2324>
- [RFC-4:21]
- Lock, C. and S. Tiva, “PCTP Extensions for Being Done”, Work in Progress, never.
- [RFC6585]
- Nottingham, M. and R. Fielding, “Additional HTTP Status Codes”, RFC 6585, April 2012.<https://www.rfc-editor.org/info/rfc6585>
- [RFC7168]
- Nazar, I., “The Hyper Text Coffee Pot Control Protocol for Tea Efflux Appliances (HTCPCP-TEA)”, RFC 7168, 1 April 2014.<https://www.rfc-editor.org/info/rfc7168>
- [RFC7725]
- Bray, T., “An HTTP Status Code to Report Legal Obstacles”, RFC 7725, February 2016.<https://www.rfc-editor.org/info/rfc7725>
- [RFC7942]
- Sheffer, Y. and A. Farrel, “Improving Awareness of Running Code: The Implementation Status Section”, BCP 205, RFC 7942, July 2016.<https://www.rfc-editor.org/info/rfc7942>
- [RFC9110]
- Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., “HTTP Semantics”, STD 97, RFC 9110, June 2022.<https://www.rfc-editor.org/info/rfc9110>
- [THREAT-MODEL]
- bongsmoke.cloud, “Threat model”. Where it and Section 12 disagree, it wins. /security
Appendix A.Implementation Status
This section records the status of known implementations of this protocol at the time of posting, following [RFC7942], which asks that such a section be removed before publication. It will not be, because publication will not occur.
- Organization
- bongsmoke.cloud
- Location
- /burn
- Description
- Complete implementation of Sections 4 through 6, including the passphrase variant.
- Level of maturity
- Production. Also 87%.
- Coverage
- Every MUST in this document, including “an implementation MUST NOT be finished”, which is satisfied continuously.
- Implementation experience
- Web Crypto and
CompressionStreamcover every primitive in Section 5 and Section 4.3. The only runtime dependency beyond the framework is the Redis client. The smoke is hand-written WebGL2. - Licensing
- MIT.
- Other renditions
- HTML at /rfc; 72-column plain text with form feeds at /rfc.txt. Both are generated from one source, so where the two differ, one of them has a bug.
Acknowledgments
The authors thank the friend who asked for a site that is “100% everything that everything else is not”, and who did not specify whether this document was supposed to be serious. It has not been possible to determine.
The microwave is thanked for its service. The pandas were not consulted.
The trash pandas were not invited. They are thanked anyway, in case it helps.
Authors' Addresses
Between them, the authors are a working group, a couch, a standing desk, a unit of time, a panda sanctuary, landlords to an unknown number of trash pandas, and the national headquarters of the Juggalos, for some reason. The giant pandas were already wearing the face paint when they arrived. Correspondence for the headquarters: /hq.