Building for Vulpine

How to write something that runs on the Vulpine desktop. Everything here is checked against wallet 0.98.0. Where this and gekker.ts disagree, gekker.ts is right.


The shape of it

The full vocabulary — 52 types — is at <https://fwetch.lol/protocol>, and how to write one is at <https://fwetch.lol/building>. Read them before inventing anything. Almost everything you would want to say already has a type, and fwetch-assert {type, parent?, payload} is the general escape hatch for anything that does not.

Verify a record against what the indexer reads, not against its name. Three verbs shipped writing records the indexer filed and the apps could not read, and all three looked like they worked. fwetch-article needs a Library coordinate rather than text, and fwetch-listing needs kind and category; the ones the index recognizes carry them. A fwetch-name claim is { name } and nothing else. The index recognizes claims with no un, and the protocol reads un: 1 as a release, so leave un off a claim.

A tod, in full

{
  "tod": 1,
  "name": "fortune",
  "title": "Fortune",
  "version": "1.0.0",
  "author": "#1",
  "about": "prints something a block once said",
  "glyph": "🜚",
  "needs": ["read"],
  "page": "<!doctype html><html>…</html>"
}

The bridge

The bridge was called Gekker until the social site at gekker.lol took that name. Nothing a tod calls changed: it is still vulpine.read() and the rest, and the runtime file is still gekker.ts.

The wallet injects this before any of your code runs. Every call returns a promise.

await vulpine.list('/boards')              // what is there
await vulpine.read('/boards/pol/a1b2c3d4') // what it says
await vulpine.fetch('/api/tartary/map')    // this network's API, read-only

await vulpine.who()                        // { number, numbers } — never a key
await vulpine.sign({ p: 'fwetch-post', b: 'hello', board: 'FG' })
await vulpine.pay('1Abc…', 5000)           // satoshis
await vulpine.pay('#4', 5000)              // to whoever holds #4 right now; the index resolves it

await vulpine.put('score', '41')           // your own corner of this machine
await vulpine.get('score')
await vulpine.open('chat')                 // one of the wallet's applications
capabilitywhat it allowsasks the person?
readthe filesystem and this network's APIno, including /numbers, /names and /pictures, which describe the person
whothe numbers they hold, lowest first; no name, key or addressonce per run
signwriting a record to the chain as themevery time, showing the record
paysending satoshisevery time, showing who and how much
storekeeping notes between runsno
openopening the wallet's applicationsno

What a tod cannot do, and will not be able to: see a private key, see a seed phrase, read another tod's notes, reach the wallet's own state, or sign anything without the person seeing exactly what it is. The frame has no same-origin access to the wallet at all; the bridge is its only channel to the wallet. The frame can still reach the network on its own: see the known gaps below.

The filesystem

Paths are questions to the network, not a tree anybody stores:

/numbers            the user numbers this key holds
/pictures           the library pieces it holds
/names              the .fox names and $handles it holds
/boards             every board; dig into one for its last 40 posts, each
                    named by the first 8 characters of its txid
/chain              districts, newest first; dig into one for its transactions
/apps               what this machine can run

A tod that replies to a post or reacts to one needs the full txid, which the filesystem does not give. Read the feed instead: vulpine.fetch('/api/feed?board=FG&limit=40').

Writing one

There is no toolchain to install. A tod is a page.

# 1. write index.html — markup, styles and script in one file
# 2. wrap it
node -e "
  const fs = require('fs')
  fs.writeFileSync('fortune.tod', JSON.stringify({
    tod: 1, name: 'fortune', title: 'Fortune', version: '1.0.0',
    author: '#1', about: 'prints something a block once said',
    glyph: '🜚', needs: ['read'],
    page: fs.readFileSync('index.html', 'utf8'),
  }))
"
# 3. install it
#    in the wallet's terminal:
#    foxhole install https://your-site/fortune.tod

For anything with a build step, bundle to a single file first. Tartaria's make-tod.mjs does exactly this with Vite: build, inline every asset back into the page, refuse to continue if anything is still loaded from outside. Copy it.

Publishing on chain

A tod is one JSON file, which is why it can be an inscription. Inscribe it, and foxhole install <outpoint> fetches it from the chain rather than from anybody's server. It is then transferable and sellable like everything else here.

The Den — a listing of tods claimed on chain, first inscription wins a name — is designed but not built. See VULPNET.md in the wallet project.

Rules worth following

Known gaps in wallet 0.99.0

Checked against the runtime. Build around these until the wallet closes them.

Closed in 0.99.0: tods now run on desktop; /numbers, /pictures and /names ask the who question; /numbers/#N no longer shows an address; who returns the name; reactions, votes and reveals sign as they should; the sign prompt shows the whole record; open refuses an unknown application; removing a tod removes its notes; /chain lists the newest blocks.

What is not there yet