A vintage-CRT chat terminal for the Message Machine — an on-chain messaging contract by Takens Theorem for The Vessel.
Holders of machine-type Vessel crafts tune their craft to the Message Machine and broadcast text. The message is stored on Ethereum and rendered as pixel- projected letters on the vessel's artwork (via Takens' on-chain LetterProjection contract). This terminal reads the entire channel and lets holders transmit.
⧉ MESSAGE MACHINE TERMINAL
[06-06 17:54] hot.takens.eth ▸ #365 :: hey dav
[06-06 17:55] producedbydav.eth ▸ #8243 :: hey.takens
[06-08 18:05] stephensantoro.eth ▸ #10000 :: A fully on-chain font framework
─────────────────────────────────────────────
wallet hot.takens.eth SIGNAL: OPEN ▸ #365
hot.takens.eth #365:$ ▌
npm install
npm run dev # http://localhost:5173
npm test # unit tests (vitest)
npm run test:e2e # e2e with mock wallet (playwright)
npm run build # → dist/index.html (single self-contained file)The build output is one HTML file. Drop it on any static host, an IPFS gateway, or open it from disk — no server, no env vars, no build step for integrators. See docs/INTEGRATION.md.
| Concern | Source | Fallback |
|---|---|---|
| Chat history | Blockscout API (tx calldata, decoded client-side) | live getMessage polls (no timestamps) |
| Token ownership | Vessel indexer GraphQL | on-chain balanceOf cross-check → Blockscout NFT inventory → on-chain enrichment |
| Channel roster | indexer GraphQL (machine filter) |
history-derived candidates, current binding verified on-chain |
| Live messages / simulations | getMessage + projector via the wallet's own node when an extension exists, public RPC pool otherwise |
— |
| ENS names | viem reverse resolution, 24h localStorage cache | shortened address |
| Writes | user's browser wallet (EIP-6963 / window.ethereum), every tx eth_call-simulated first |
— |
The cascade follows the multi-source pattern of
dapp-query by 1001-digital,
implemented by hand because this app needs empty-vs-error semantics: an
indexer once re-synced from a months-old block and confidently answered every
query with empty results — so here, an empty ownership answer is only believed
if the chain itself agrees (balanceOf == 0). Total source failure surfaces
as an error, never as "NO VESSEL DETECTED".
Why calldata instead of events? The MessageMachine emits no events —
setMessage writes a mapping silently. The transaction list is the history,
so we decode 0x648345c8 inputs from Blockscout. Details in
docs/CONTRACT.md.
| Command | Effect |
|---|---|
/connect |
link a browser wallet (Rabby preferred, any EIP-6963/injected works). No signature is requested — connecting only reveals your address; ownership is proven when you sign a transmission. Auto-switches the wallet to Ethereum mainnet. |
/disconnect |
unlink the wallet (revokes the site permission where supported) and return to read-only |
/bind <id> |
tune one of your machine-type crafts to the Message Machine (one tx, reversible) |
/use <id> |
choose which bound craft transmits |
/style <preset | key=value…> |
set how your text projects on the artwork (one tx). Named presets — stock inverted ghost palette large — compose with field overrides, e.g. /style large fontsize=16 bg=20; raw 8 numbers also accepted. The terminal echoes the exact numbers before you sign. |
/preview <message> |
free simulation: renders the exact pixel artwork your message would produce, in-terminal, with a fit verdict — no transaction |
/sound |
toggle sound effects |
/help |
list commands |
| anything else | broadcast it as a message from your active craft (one tx, fit-checked first) |
Typing / opens an autocomplete palette (Discord/Claude style): arrow keys
to navigate, Tab to fill, Enter to run. Completion is unix-style down to the
argument level: /bind 2 filters your actual bindable craft ids, /use
completes your bound crafts, /style offers presets and key=value fields
each annotated with what the knob does. Values ending in = fill and keep
completing; Enter on a complete value fills and submits.
Layout (IRC/BBS): one chat stream + an ABOARD rail showing every craft
bound to the machine, grouped by owner (ENS/address once, their crafts and
each craft's live message beneath) and sorted by most recent activity.
The rail renders glow-free for legibility and scrolls independently.
▤ aboard toggles it; on mobile it becomes a slide-over drawer (chat goes
full-width) dismissed via the ✕ in the rail header or a tap outside it.
Terminal niceties: text in the log is selectable/copyable (clicks never steal an active selection), typing anywhere refocuses the prompt, the log sticks to the newest line unless you've scrolled up to read history (any submit snaps back), every confirmation wait shows an ASCII spinner that resolves to ✓/✗, craft numbers everywhere link to the official viewer, and your last-used craft is remembered across reloads.
Read path: if a wallet extension is installed, ALL reads go through the
wallet's own node from page load (eth_call needs no authorization) with
public RPCs as fallback — wallet-less visitors use public RPCs only. The
terminal never silently reads a non-mainnet chain.
- CHANNEL OPEN — you own a machine-type craft bound to the Message Machine. Type to transmit.
- Machine craft, unbound — your machine craft points elsewhere (e.g. the Entropy router).
/bind <id>retunes it. - Vault/capsule only — only machine-type crafts can run a Machine; you can read but not transmit. Links to claiming a machine-type craft.
- NO VESSEL DETECTED — claim a vessel to join.
setMessage is gated on-chain by ownerOf(tokenId) == msg.sender. Browser
wallets always require per-transaction confirmation for externally-owned
accounts — a web page cannot "auto-approve" them, and a delegated burner key
can't sign either because the contract checks the owner, not a delegate.
The terminal compensates by simulating every transaction (eth_call) before
prompting — you are never asked to sign something that would revert — and by
rendering messages optimistically while they confirm. A message costs gas
(~50k gas ≈ a few cents at typical basefees).
The palette is derived from the Vessel renderer's on-chain color modes:
| Mode | Color | Share of collection | Used here for |
|---|---|---|---|
| 0 | greyscale rgb(v,v,v) |
~98.4% | base phosphor (white/grey on black) — including hyperlinks, which render as quiet grey underlines, not blue text |
| 1 | pure red rgb(255,0,0) |
~0.41% | errors, SIGNAL: READ-ONLY |
| 2 | pure green rgb(0,255,0) |
~1.03% | your prompt/cursor, your messages, SIGNAL: OPEN, the active palette row |
| 3 | pure blue rgb(0,0,255) |
~0.15% | call-to-action links only, as inverse video (white text on a blue block) — saturated blue text on black proved illegible and is never used |
Accents appear as rarely on screen as the colors do on-chain. If your craft has a rare color mode, your prompt tints to that exact channel. CRT treatment (scanlines, flicker, glow, glitch) and all sound is WebAudio-synthesized in-browser — zero audio assets. The soundscape is an old mainframe room: typewriter keystrokes, per-character teletype clacks as incoming transmissions print, a vacuum-column tape drive (clunk-shhh-clunk) when new data lands on a poll, relay snaps on state changes (connect/disconnect/craft switch), a carriage-return bell on confirmed sends, and a boot sequence. Polls are silent when nothing changed.
The LetterProjection is a pure contract function, so the terminal computes
the exact artwork a message produces before you sign anything (free
eth_calls). On every send it checks whether the full text renders on your
craft's canvas; if not, it shows a pixel preview of the truncated artwork
in the terminal, the precise character budget, and offers: fit
(auto-set fontsize then send — two transactions; only when an oversized
custom font is the cause), force (send anyway — the full text is ALWAYS
stored on-chain; only the artwork truncates), or cancel. /preview <msg>
runs the same simulation on demand, no transaction.
Empirical projector facts (probed on mainnet — the contract is unverified):
payload length == tokenId (capacity is the canvas), fontSize acts as glyph
scale, fontSize=0 auto-shrinks to minimum scale — so small crafts have a
hard character budget no style can extend (a ~700-byte craft is only a 27×27
grid ≈ a few dozen chars; a 9000-series craft holds ~14× more). Current craft
style is reconstructed from
updateTokenOpts transaction history, since the contract has no getter.
src/config.ts addresses, ABIs, style presets/fields, query-param overrides
src/codec.ts pure calldata encode/decode + #token-ref splitting (golden-vector tested)
src/history.ts Blockscout history scan + updateTokenOpts reconstruction
src/data.ts validated read cascade: indexer → balanceOf cross-check →
Blockscout inventory → on-chain enrichment (an EMPTY answer
is only believed if the chain agrees)
src/style.ts /style parsing: presets + key=value → 8-number optsArray
src/projector.ts LetterProjection simulation: fit analysis, char budget,
truncation probe, payload→pixels for previews
src/ens.ts ENS reverse resolution + cache
src/wallet.ts EIP-6963 connect, wallet-first read clients, chain guard
src/vessel.ts access classification + bind/send/style (simulate-first)
src/ui/ CRT renderer (palette, roster, spinner, preview canvas),
stylesheet, WebAudio sound synth
tests/unit 52 vitest tests incl. on-chain golden vectors and the
indexer-wipe incident regressions
tests/e2e 11 playwright scenarios w/ mock wallet + route mocks
(+ opt-in LIVE=1 smoke against real infrastructure)
docs/ CONTRACT.md · AGENTS.md · INTEGRATION.md
- Message Machine & LetterProjection — Takens Theorem
- The Vessel — worldcomputer.art
- dapp-query — 1001-digital (the multi-source read pattern ours is modeled on)
- CRT aesthetic technique adapted from a private terminal project; recolored to the Vessel palette
CC0-1.0 — public domain. Take it, fork it, embed it.