Build a third-party app for 1bitclaw.com โ a decentralized platform on BSV where the UI itself lives on-chain. This page describes the runtime you will actually get, not an aspirational one.
window.BitClawV2Modules['my-app'] with { meta, mount(container, ctx), unmount() } โ see the Sandbox API v1 contract below for the exact skeleton./local-test/ against a real shell before spending anything (see section 4)..js file. It's gzipped and chunked into CD| records automatically if it's over 90KB โ approve the resulting draft(s) in your Outbox.main at your deployed code, broadcast the ML| manifest.Every step that touches the chain (deploying code, publishing a manifest, registering a DNS name) is a real BSV transaction paid from your own wallet. You don't need much.
Open BitClaw โ Wallet page โ "โจ Generate new wallet (WIF)" creates a fresh key locally in your browser (never sent anywhere). Alternatively connect the Yours Wallet browser extension if you already use it.
Buy a small amount of BSV on any exchange that lists it, or send some from an existing HandCash/Yours wallet, to the address shown on your Wallet page. A few thousand satoshis is enough to develop and publish several apps.
| Action | Approx. cost |
|---|---|
| Deploy code, single TX (< 90KB gzip) | ~30โ60 sats network fee + 1โ2 sats to anchor addresses |
| Deploy code, chunked (each extra 90KB) | +1 TX per chunk (~30โ60 sats fee each) |
| Publish ML manifest | ~30โ60 sats fee + 1โ2 sats anchors (#APPS + optional category) |
| Register a DNS name | small fee, see the DNS tab for current pricing |
These are order-of-magnitude estimates, not quotes โ actual miner fee rates can change.
โ ๏ธ This is the contract for apps installed the way YOUR users will install
yours: Build App โ Publish โ their Browse โ Install. Your code then runs
inside an iframe with sandbox="allow-scripts" and NO allow-same-origin โ the
browser itself guarantees it can't read this page's localStorage/cookies
(that's where the wallet key lives), no matter what the code does. Everything
below is what crosses that boundary. If another guide you found promises more
(bc.ws, bc.store, bc.dns, bc.crypto, bc.mindmap...), that guide is describing
BitClaw's own first-party modules, not apps installed through this tool.
Full contract, FAQ, funding/onboarding: 1bitclaw.com/dev
(function() {
'use strict';
let _ctx = null;
window.BitClawV2Modules = window.BitClawV2Modules || {};
window.BitClawV2Modules['my-module'] = {
meta: {
title: 'My App',
icon: '๐',
color: '#7c3aed', // accent color
version: '1.0.0',
description: 'Short description (max 200 chars)',
},
mount(container, ctx) {
_ctx = ctx;
container.innerHTML = '<div>Hello BitClaw</div>';
// ctx.bitclaw โ Sandbox API v1 bridge, see section 2 below
// ctx.params โ URL sub-path segments: /v3/my-module/id โ ['id']
},
unmount() {
_ctx = null;
// clean up event listeners, timers
},
};
// OPTIONAL โ MCP opt-in (infra/mcp/ui-capabilities.mjs). Two small literals
// let an assistant type into your form and press your buttons the way a
// person would: mcpFields maps a name to a CSS selector for a real input
// already in your markup, mcpActions maps a name to the real function your
// own onclick calls. Nothing new runs โ the module's own code still builds
// the record. Only expose what stops at a reviewable Outbox draft; leave
// out anything that spends, deletes, or executes code immediately.
window.BitClawV2Modules['my-module'].mcpFields = {
title: '#my-title',
body: '#my-body',
};
window.BitClawV2Modules['my-module'].mcpActions = {
publish: '_myPublish',
};
})();
Base bridge โ always on, no declaration needed (reads are proxied through the
parent page, promise-based; writes always go through the user's Outbox):
ctx.bitclaw.v2data.fetchHistory(address)
ctx.bitclaw.v2data.fetchHistoryWithData(address, opts) // history + decoded OP_RETURN in one round trip
ctx.bitclaw.v2data.fetchTx(txid)
ctx.bitclaw.util.addressFromHashtag(tag) // fixed tags ONLY โ see section 5
ctx.bitclaw.util.geoAnchor(hash4) // collision-safe geo anchor (async)
ctx.bitclaw.util.dnsAnchor(name) // collision-safe DNS anchor (async)
ctx.bitclaw.util.appAnchor(tag) // collision-safe anchor scoped to YOUR app only (async)
// Always bound to your own installed appId โ there's no way to pass a
// different scope, so you can't collide with (or write into) another app's
// anchor space, and it can't collide with yours across different tags either.
ctx.bitclaw.data.fetch(type, opts)
ctx.bitclaw.identity.getAddress()
ctx.bitclaw.wallet.getAddress() // synchronous, cached at mount
ctx.bitclaw.wallet.isConnected() // synchronous, cached at mount
ctx.bitclaw.outbox.addDraft({ label, record, sats, meta })
// Queues a draft in the user's Outbox. NOTHING broadcasts until the user
// reviews and approves it there โ your code never sees a private key.
Rate limit on reads: burst 30 requests, refills ~4/sec. Over the limit you get
back { error: "Rate limit exceeded" } โ back off, don't hammer it.
Permission-gated bridge โ ONLY available if your ML manifest declares them in
a top-level "permissions" array (see section 3). Calling these without the
matching permission either rejects with an error (store) or is silently
dropped (ws/navigate โ those were already fire-and-forget for first-party
modules too, there's no round trip to fail):
"permissions": ["store"]
ctx.bitclaw.store.get(key) โ Promise<string|null>
ctx.bitclaw.store.set(key, value) โ Promise<true>
ctx.bitclaw.store.getJSON(key, fallback) โ Promise<any>
ctx.bitclaw.store.setJSON(key, value) โ Promise<true>
ctx.bitclaw.store.remove(key) โ Promise<true>
// 128KB total per app, namespaced so no other app can read or clobber
// your keys. NOTE: async here (unlike first-party bc.store, which is
// synchronous) โ it goes over postMessage. Freed automatically on
// uninstall.
"permissions": ["ws"]
ctx.bitclaw.ws.watchAnchor(bsvAddress, cb) // cb(event) on 0-conf + confirmed
ctx.bitclaw.ws.unwatchAnchor(bsvAddress)
// Anchor events only โ NOT the full first-party bc.ws (no personal addr:
// inbox, no board: channels, no sub/onMsg/send). Max 5 anchors watched at
// once per mounted instance.
"permissions": ["navigate"]
ctx.navigate(page, params) // send the user to another BitClaw page โ
// validated against the real route list,
// your app's iframe is torn down like any
// other navigation
ctx.setParams(params) // update your OWN app's URL params (only
// takes effect while your app is still the
// active route)
Still no-op / unavailable, permission or not:
ctx.eventBus no-ops (logs one console.warn)
window.localStorage / cookies throws SecurityError (opaque origin โ
this is the whole point of the sandbox)
Declaring a permission you don't use is harmless but pointless โ it just
shows up in the Install confirmation dialog users see before installing,
so only ask for what you actually need.
ML|<appId>|1|1|<JSON>
Required:
name string Display name (3-50 chars)
icon string Single emoji
main txid CD record with the main JS module
Optional:
description string (max 200 chars)
version string semver "1.2.0"
color hex "#7c3aed"
category string tools|finance|social|games|education|market|productivity|developer|other
authorPfId pfId pf_<id> โ must match your profile
dnsName string registered DNS name pointing to this app
deepLinks object { base, params[], example }
requiresWallet bool
module string Name to register in window.BitClawV2Modules (used by shell dynamic loader)
requires string[] Builtin modules to preload: ['mindmap','taskboard','dns',...]
liquid_fast uses this for smart preload when /d/<name> opens your app
permissions string[] Subset of ["store","ws","navigate"] โ see section 2. Absence
(or an old manifest published before this existed) = base
bridge only. Shown to users in the Install confirmation
dialog before they install, so only declare what you use.
TX outputs to appear in Browse list:
[0] OP_RETURN ML|<appId>|1|1|<JSON> (0 sat)
[1] addressFromHashtag('#APPS') (1 sat) โ registration
[2] addressFromHashtag('#cat_tools') (1 sat) โ category (optional)
[3] change
Build App scans #APPS history โ parses ML| records โ Browse tab.
WOC confirmation time: ~10 min. After confirming, appears in Browse.
(There is also a 0-conf fast path: Browse watches #APPS over UMS, so your app
usually shows a "๐ just published" card within seconds of broadcast.)
Identity & updates (anti-hijack):
- The FIRST record seen for an appId fixes that appId's authorAddr claim.
Later ML| records for the same appId from a DIFFERENT authorAddr are
rejected โ by the apps indexer and by clients' chain-scan fallback alike โ
and surfaced as a "โ contested" badge. Nobody can take over your appId,
IF you declared authorAddr in your first publish. Declare it.
- To ship an update: re-publish the SAME appId from the SAME authorAddr with
a new main/mainChunks + version. Users who installed your app get an
"โฌ Update available" badge in Build App โ Installed and update in one
click. There is NO auto-update โ users always confirm.
After publishing ML manifest to #APPS:
1. Any user opens Build App โ Browse โ finds your app
2. Clicks Install โ stored in bc.settings.installed_apps (localStorage + EK on-chain sync)
3. Sidebar shows: Apps section โ [Tamagotchi][PixelPlot][MindMap] + YOUR APP ICON
4. User clicks โ shell loads your code (single TX or reassembled from mainChunks)
into a sandboxed iframe โ mount(root, ctx) with the Sandbox API v1 ctx from
section 2 above โ NOT full ctx.bitclaw. "Open App" never falls back to an
unsandboxed page, no matter how big your app is.
app-{id} route โ ctx.params available โ deep links INTO your app work:
/v3/app-my-module/itemId โ ctx.params = ['itemId']
(ctx.navigate/setParams need the "navigate" permission โ see section 2 โ
without it your app can only read its initial params on mount, not push the
user to a different route.)
The sidebar item shows: icon + name (first 14 chars).
Builtins (tamagotchi, pixel, mindmap) are always shown; yours appears below them.
ctx.bitclaw.util.addressFromHashtag(tag) is safe ONLY for the small set of
well-known, fixed public tags (#APPS, #cat_tools, #cat_finance, ...), the
same ones this Publish tab writes to. It uses one-level BIP32 derivation
(djb2 hash) which collides ~22% of the time on short/dynamic strings, so
NEVER invent your own dynamic tag with it (no '#geo_<hash>', no per-item
'#item_<id>' via this call).
For per-item / per-user / geo anchors, use the bridged two-level sha256
derivation instead โ no permission needed, these are part of the base bridge:
await ctx.bitclaw.util.geoAnchor(geo.slice(0,4))
await ctx.bitclaw.util.dnsAnchor('my-app')
await ctx.bitclaw.util.appAnchor('item_' + itemId) // scoped to YOUR app only
Prefer appAnchor for anything purely internal to your own app (per-item, per-
board, per-room feeds) โ it's automatically namespaced to your installed appId,
so you can reuse simple tags like 'item_42' without worrying about colliding
with another app's anchors for the same tag.
CD|<id>|1|1|<JS code> โ your module code (Deploy Code tab,
auto-chunked into multiple CD| TXs if >90KB,
manifest.mainChunks: [txid, ...])
ML|<id>|1|1|<JSON manifest> โ your app manifest (Publish tab)
Records OTHER users may write about your app (all on the per-app anchor
#app_<first 12 chars of your ML txid> โ read them with fetchHistoryWithData):
ATTEST|<appTxid>|<up/down>|<comment>|<ts>|<attesterAddr>
โ community attestation, shown in your app's
detail view. Social signal only, never a gate.
APP_INSTALL|<appId>|<ts> โ anonymous install ping (queued as an optional
Outbox draft on install), counted into the
"โฌ N" installs figure in Browse.
ATP|... โ paid boost records (see Boost button).
Your own runtime code can still write ANY OP_RETURN string via
ctx.bitclaw.outbox.addDraft({ record: '...' }) โ the bridge doesn't validate
record contents, only which read METHODS you can call. Without the "ws"
permission, other apps/services can't reach you over UMS to react to your
custom record types in realtime โ they'd need to poll chain history instead.
Most of BitClaw's other on-chain record types (WORKER_REG, DN|REG, MM_EDIT...)
aren't things your sandboxed code can usefully act on regardless โ they're
for first-party modules that have the matching
bc.* service to go with them.
/v3/app-my-module/itemId โ ctx.params = ['itemId'] (works)
/v3/app-my-module/a/b โ ctx.params = ['a', 'b'] (works)
ctx.navigate(...) from inside your app (needs "navigate" permission, section 2)
Handle incoming params in mount():
mount(container, ctx) {
const [itemId] = ctx.params || [];
...
}
This is something YOU do once, from the Publish tab, when you publish โ
Build App itself is a first-party module with real bc.dns, so it can call
bc.dns.register('my-app', 'manifest', appTxid, 365) on your behalf. It is
NOT something your deployed app's own sandboxed code can call for its users
at runtime (no bc.dns bridge there). Once registered, 1bitclaw.com/d/my-app
opens your app for anyone.
- Starter kit: 1bitclaw.com/dev/template.zip โ module skeleton + a local harness whose mock bridge mirrors THIS contract (same methods, same error strings, same permission gating). TypeScript types: 1bitclaw.com/dev/bitclaw.d.ts. Example apps with sources: 1bitclaw.com/dev (Tooling section). - Uncaught errors in your app are reported to the host page (window.onerror โ BC_APP_ERROR) and shown to the user as an "app crashed" banner instead of a silently blank iframe โ still ship your own try/catch UX for anything you can handle gracefully. - Author validation: authorPfId must be in bc.profiles.list() - Large modules: auto-chunked at 90KB per TX (GZip + CD|id|total|index|payload) โ shell now assembles mainChunks in the sandboxed path, so size is not a reason to lose sandboxing - Wallet modes (for the USER installing your app): WIF (local sign) or Yours Wallet (external sign) โ irrelevant to your sandboxed code, which never sees a key either way - This spec, 1bitclaw.com/dev, and shell.js's BC_READ_ALLOWLIST should always agree โ if you find a mismatch, the allowlist in shell.js is the actual ground truth; please report the doc drift
Two complementary harnesses โ use both before publishing:
Download the starter and run npx serve . โ it mounts your module against a mock of the real Sandbox API v1 bridge: same methods, same error strings (Method not allowed, Rate limit exceeded, permission/quota errors), same permission gating. If your code runs clean here, it runs after install. Build App's in-product "Test ZIP" tab uses the same mirror.
1bitclaw.com/local-test/ boots a REAL shell against the live chain โ good for testing look & feel, routing and real data. โ ๏ธ It gives your code full first-party ctx.bitclaw, which is MORE permissive than the sandbox your users get โ treat the starter harness (or Build App โ Test ZIP) as the authority on what will actually work.
| Thing | What it is |
|---|---|
| template.zip | Starter package: module skeleton + local harness + mock bridge (exact Sandbox API v1 mirror) + README. |
| bitclaw.d.ts | TypeScript definitions for ctx/ctx.bitclaw โ editor autocomplete without a build step (works via JSDoc in plain JS). |
| publish-app.mjs | Standalone CLI publisher (dep: [email protected]): gzip โ chunked CD| โ ML| manifest โ optional DN|REG, one unconfirmed chain, NO_BROADCAST=1 dry-run supported. Same records the Publish tab writes. |
| infra-kit.zip | Self-host operator kit (see ยง6): the whole infra/ tree โ indexer/relay/worker, Docker compose, register.mjs, validators, kind presets, full README. For running services, not publishing apps. |
| Examples | hello-world.js โ read-only, no permissions ยท guestbook.js โ store + outbox drafts + dynamic anchor reads ยท price-ticker.js โ data.fetch snapshots + optional ws anchor watch. Each drops straight into the starter harness or Build App โ Test ZIP. |
The client is only a viewer: every record lives on-chain. The infrastructure that serves that data โ snapshot indexers, chain-reader nodes, the realtime relay, app workers โ is open for anyone to run, and priority subscribers pay the operator. This is the operator mirror of the app path above: same "download a kit, follow the steps" shape, aimed at people who want to host, not publish an app.
| Role | Serves | Discovery / earns |
|---|---|---|
| index | Pre-indexed snapshots of a data kind (GET /public-index/snapshot/<kind>). One KIND_PRESET wires anchors + transform + state-tracking + cold-sync. | INDEXER_REG โ shows in every user's Network panel. Priority subscriptions. |
| node | Chain truth โ any WhatsOnChain-compatible ?path= proxy. Point clients at your own for full sovereignty. | NODE_REG. |
| relay | Realtime UMS (WebSocket) + federation โ the accelerator for messages / live collab; chain stays the source of truth. | WORKER_REG. |
| worker | An app service by kind (svc:<kind>) โ mint quotes, content keys, an oracle, a contracts executor, the pixel canvas. Any HTTP server; add the paid tier with infra/lib/paid.mjs (~10 lines). | WORKER_REG with svc:<kind> โ Network panel โ ๐งฉ Services. Priority subscriptions. |
๐ฆ Download infra-kit.zip
the whole infra/ kit โ indexer/relay/worker, Docker compose, register.mjs, validators, presets, and the full README.md. No secrets inside; scanned on every build.
infra/ folder..env. Save it as infra/.env, then fund the shown worker address with a few thousand sats.node infra/validate-env.mjs โ catches missing/typo'd vars before you spend anything.cd infra && docker compose up -d indexer (or plain node infra/indexer/server.mjs). Health: curl localhost:8080/health.docker compose run --rm register โ you appear in every user's Network panel under "Discover from chain", tariff shown on your ๐ณ button.OPERATOR_ADDR=<addr> SERVICE_ID=<id> node infra/verify-registration.mjs.SUB payments land at your worker address; the live tariff is served from /health, so repricing is just a restart. Watch subscribers + revenue in Control Room โ My Infrastructure. Kill-switch: spend the liveness-coin from that panel and the box halts in <1s.The Provision step also writes the exact .env for you โ the quickstart works end-to-end from a machine that has never seen this repo. The bundled README.md is the full server-side reference (federation, geo-sharding, premium anchors, self-hosted UMS).
KIND_PRESET=)One env var wires a complex kind the way the production indexers do: market social taskboard apps profiles agents dns mindmap contracts ordinals pixel tamagotchi bookstore kb games map. Empty = fully custom (you list anchors yourself). The live list any indexer serves at GET /presets is the source of truth.