Build on your name
Every BSVKey name can serve a web page that lives on the Bitcoin BSV blockchain. This guide explains exactly what those pages can and can't do, why, what kinds of sites thrive, and gives you tested code for payments, live data, guestbooks and more.
- Where your site runs
- Why pages run in a sandbox
- What works and what doesn't (tested)
- Sites that thrive
- Sites that need more
- How to get more capability
- Payments from any wallet
- Code cookbook
- Size and cost
- Make the most of your site
- Questions
Where your site runs
You publish one HTML file on-chain from the wallet that owns the name (bsvkey.com §11). Visitors can open it in two places:
bsvkey.com viewer
bsvkey.com/#site=yourname
Works for everyone, on any device, with nothing to install. This viewer is the strictest, because it sits on the same website as the BSVKey wallet app.
.key browser extension
yourname.key/ in Chrome, Edge, Brave
The free BSVKey Names extension opens names as .key addresses (Edge: Microsoft Edge Add-ons; Chrome Web Store listing in review). Its sandbox is looser: outside images, fonts, embeds and web APIs work there.
Both viewers load the same on-chain file, so design for the bsvkey.com viewer first and let the extension add extras. The toolkit tells your page which viewer it is in.
Why pages run in a sandbox
Anyone can publish anything to their name. The sandbox is what makes it safe for visitors to open any name.
Pros
- Visitors are protected. A page can't read the BSVKey wallet, keys or saved data, can't redirect the visitor's tab, and can't set cookies to track them.
- Permanent. The page is on the blockchain. There is no server to fail, no hosting bill, no account to suspend.
- Authentic. A page counts only if the name's owner signed it, so nobody can put words on your name.
- Censorship resistant. Every copy of the chain holds your page.
- Cheap and instant. About 1 sat per byte plus a 5,000 sat publish fee, live after one block.
- Private by default. No analytics, no cookies, no third-party trackers in the bsvkey.com viewer.
Cons
- One self-contained file. All HTML, CSS, JavaScript and images go inline. No CDNs, no external scripts.
- No server code. Nothing runs except in the visitor's browser, and everything on-chain is public, so a page can't keep secrets.
- Nothing is remembered between visits. localStorage, cookies and IndexedDB are off in both viewers.
- No wallet inside the page. Payments go through bsvkey.com/pay, which opens in its own tab.
- The bsvkey.com viewer is strict. No outside images, fonts, video files, embeds or web APIs, apart from the chain data APIs listed below.
- Not in search engines. Sandboxed on-chain pages aren't indexed. Mirror the page on a normal domain if you need search traffic.
- Updates are new versions. Republishing replaces what visitors see, but every old version stays on-chain forever.
What works and what doesn't
Measured on 11 Oct 2026 by running a probe page inside each viewer, with the real security settings. The extension column was measured in an exact emulation of its sandbox.
| Feature | bsvkey.com viewer | .key extension | What to do |
|---|---|---|---|
| Your own HTML, CSS and JavaScript (inline) | Works | Works | Put everything in the one file. |
| Scripts from CDNs (cdnjs, unpkg, jsDelivr) | Blocked | Blocked | Paste the library's code into a <script> in your page. |
eval, new Function, WebAssembly | Blocked | Blocked | Use libraries and build tools that don't need eval. Most modern bundles are fine. |
| Outside stylesheets, Google Fonts | Blocked | Works | Inline your CSS. Use system fonts, or embed a font as a base64 @font-face. |
Images: inline SVG and data: URIs | Works | Works | Best choice for logos, icons and small pictures. |
| Images from other websites | Blocked | Works | Inline them, or show them only in the extension. |
Images generated in the page (blob:) | Blocked | Works | Draw on a <canvas> instead. |
| Audio and video files | Blocked | Works | Generate sound with Web Audio, or link to the media. |
| YouTube, Vimeo, maps and other embeds | Blocked | Works | Show a link in the bsvkey.com viewer and the embed in the extension (recipe). |
| Bitcoin BSV chain data (WhatsOnChain API) | Works | Works | Balances, transactions, block height, BSV price (recipe). |
| Exchange rates (open.er-api.com) | Works | Works | Show prices in any currency. |
| BSVKey name index and Name Market listings | Works | Works | Look up names and owners, show "for sale" (recipe). |
| Other web APIs | Blocked | Works* | *Only APIs that allow cross-site requests (CORS). |
| WebSockets (live push) | Blocked | Blocked | Poll every few seconds instead. |
| Payments from any wallet | Works | Works | Link to bsvkey.com/pay. Your page sees the payment arrive. |
| A wallet inside the page (BRC-100, keys) | Blocked | Blocked | Intentionally: use bsvkey.com/pay. |
| localStorage, sessionStorage, cookies, IndexedDB | Blocked | Blocked | Keep state in memory for the visit (BK.store), or on-chain with pay notes. |
Multiple pages with #hash links | Works | Works | One file, many sections (recipe). |
history.pushState | Blocked | Works | Use hash routing so it works everywhere. |
| Canvas 2D, WebGL, Web Audio | Works | Works | Games, generative art and music tools work great. |
| Crypto: random numbers, SHA-256 | Works | Works | Proof and verification pages (recipe). |
| Web Workers | Blocked | Blocked | Split long work into chunks with setTimeout. |
alert, confirm, prompt | Blocked | Works | Use your own on-page messages instead. |
| Clipboard, location, camera, microphone, notifications | Blocked | Blocked | Show text people can select and copy. |
| Links to other names | Opens in viewer | Opens in viewer | Link to https://name.key/ or bsvkey.com/#site=name. |
Links to other websites, mailto: | New tab | New tab | They open in a normal tab, so logins and checkouts work. |
| Forms that submit to a server | Blocked | Not useful | Use mailto: links, a pay note, or link out to a form. |
| Search engine indexing | No | No | Mirror the page on a normal website if you need search traffic. |
Sites that thrive
Ready today, in both viewers
- Personal pages, portfolios, link-in-bio, résumés
- Project, product and landing pages
- Manifestos, whitepapers, documentation, letters, time capsules
- Menus, price lists, event pages, flyers
- Tip jars and donation pages for any wallet
- Pay-what-you-want pages and simple storefronts that take orders as pay notes
- Guestbooks, comment walls, polls (each vote is a small payment with a note)
- Live chain dashboards: balances, prices, block height, a wallet's activity
- Canvas and WebGL games, generative art, music toys
- Calculators, converters, checklists, small tools that need no server
- Proof pages: certificates, document fingerprints, notarized statements
- Name showcases and "this name is for sale" pages
Even better in the .key extension
- Photo galleries using images hosted elsewhere
- Pages with web fonts and outside stylesheets
- Video and podcast pages with embedded players
- Maps, social embeds, outside widgets
- Dashboards pulling any public CORS API (weather, markets, sports)
Design so the page still makes sense in the bsvkey.com viewer: show a link where an embed would be (recipe).
Sites that need more
These need something outside the page: a server, an account system, or a different viewer.
| Kind of site | Why the sandbox isn't enough | What to connect |
|---|---|---|
| Accounts and logins | No cookies, no storage, no wallet in the page | Link out to your app on a normal domain; or identify people by the payments they make |
| Databases and saved user data | Nothing persists between visits | Write data on-chain with pay notes; or an outside API (extension only) |
| Truly private or paid content | The page is public, so client-side locks can be bypassed | A server that sells access, such as a BSVKey Paywall service, linked from your page |
| Real-time chat, live multiplayer | No WebSockets | Poll the chain or an API every few seconds; or link to a live app |
| Shops with stock, shipping and receipts | No server to track orders | Take orders as pay notes and fulfil them by hand; or link to a checkout |
| Video and audio streaming | Media files and embeds blocked in the bsvkey.com viewer | Embed in the extension, link out everywhere else |
| Third-party widgets (maps, social, booking) | Embeds and outside scripts blocked | Link to them; embeds work in the extension only |
| Apps built on big frameworks | No CDN scripts, no eval, no WebAssembly | Bundle into one file with a build tool that avoids eval |
| Sites that live on search traffic | On-chain pages aren't indexed | Publish a mirror on a normal domain and link to your name |
| Analytics | No trackers allowed | Count visits by what people pay or sign; it's private by design |
How to get more capability
As a builder, today
- Use bsvkey.com/pay for every payment: it supports every wallet and your page is told when the payment lands.
- Store data on-chain: a small payment with a note is a permanent, public record your page can read back (guestbooks, votes, orders, comments).
- Point visitors to the extension when you need embeds, outside images or other APIs.
- Link out to normal websites for logins, checkouts and private content. Links open in a normal tab, so those flows work.
- Mirror your page on a normal domain for search engines, and link both ways.
- Bundle libraries into your page (a single-file build), and pick ones that don't use eval.
Ideas we're considering for BSVKey
Not built yet. Tell us which would help you most.
- A separate address for on-chain sites (for example
name.bsvkey.com) with a looser policy, so outside images, fonts and web APIs work for everyone, while the wallet app stays strictly isolated. - A small storage service so a site can remember a visitor's settings, kept separate from the wallet.
- Wallet requests from inside a site, with the visitor approving each one in their own wallet.
- Images and files stored as their own transactions and reused across pages, so pages stay small.
- Search-friendly preview pages for every name, and custom domains that point at a name.
Payments from any wallet
Link to https://bsvkey.com/pay?to=yourname&sats=5000&label=Tip. It opens in a normal tab and offers every way to pay:
1 · BRC-100 wallets
Metanet Desktop, the BSV Browser, any BRC-100 wallet. The wallet shows the payment and the visitor approves it there.
2 · BSVKey wallet
Paste a BSVKey key or load the key file. The key stays in that tab.
3 · Any other wallet
HandCash, Centbee, ElectrumSV, an exchange: a QR code, the address and the exact amount. The pay page watches the chain and notices the payment by itself.
| Parameter | Meaning |
|---|---|
to | A BSV address, or a BSVKey name (pays the name's current owner) |
sats or bsv | The amount, in satoshis or in BSV |
label | What the payment is for, shown to the payer |
memo | Optional note written on-chain with the payment (methods 1 and 2), readable by your page with BK.memos |
id | Optional: your own reference, so your page can match the payment |
How your page finds out: in the bsvkey.com viewer the pay tab tells your page instantly (a bsvkey:paid event). Everywhere else BK.onPaid watches the chain for a payment of at least that amount to that address, usually within seconds. If many people pay the same amount at once, use a dedicated address or a slightly different amount per order.
Code cookbook
Every recipe below was run inside both viewers before publishing. Replace yourname with your name. Paste the BK toolkit once into its own <script> near the top of your page; each recipe after that runs in its own scope, so you can combine as many as you like.
Starter page (complete, ready to publish)
A full page with light and dark themes, a tip button and the bsvkey:site tag that tells §11 which name it's for.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="bsvkey:site" content="yourname">
<title>Your name · on Bitcoin BSV</title>
<style>
:root { color-scheme: light dark; --bg:#0e1116; --fg:#e8ebf1; --mut:#9aa4b2; --acc:#f7931a; --card:#161b22; }
@media (prefers-color-scheme: light) { :root { --bg:#fafafa; --fg:#14171c; --mut:#5b6472; --card:#fff; } }
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--fg); font:17px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
main { max-width:720px; margin:0 auto; padding:48px 20px; }
h1 { font-size:clamp(32px, 7vw, 56px); line-height:1.05; margin:0 0 12px; }
.lead { color:var(--mut); font-size:19px; }
.card { background:var(--card); border-radius:14px; padding:18px 20px; margin:18px 0; box-shadow:0 1px 0 rgba(0,0,0,.08); }
a { color:var(--acc); }
.btn { display:inline-block; background:var(--acc); color:#1a1205; font-weight:700; padding:12px 20px; border-radius:10px; text-decoration:none; }
footer { color:var(--mut); font-size:14px; margin-top:48px; }
</style>
</head>
<body>
<main>
<h1>Hello, I'm on-chain.</h1>
<p class="lead">This page lives on the Bitcoin BSV blockchain. No server, no host, no takedown.</p>
<div class="card">
<h2>What I do</h2>
<p>Write a few lines about yourself, your project or your shop.</p>
<a class="btn" href="https://bsvkey.com/pay?to=yourname&sats=10000&label=Tip" target="_blank" rel="noopener">Tip 10,000 sat</a>
</div>
<footer>Also at <a href="https://yourname.key/">yourname.key</a> · Built with <a href="https://bsvkey.com/sites" target="_blank" rel="noopener">BSVKey</a></footer>
</main>
</body>
</html>BK toolkit (paste once)
Viewer detection, storage that never throws, chain data, name lookup, pay links, payment detection, on-chain notes. About 4 KB.
/* BK: a tiny toolkit for BSVKey on-chain sites. Paste it inside a <script> in your page.
Works in the bsvkey.com viewer, the .key browser extension and on the normal web. */
const BK = (() => {
const WOC = 'https://api.whatsonchain.com/v1/bsv/main'
const INDEX = 'https://bsvkey.github.io/name-index/names.json'
const mem = {}
// where this page is running: 'bsvkey.com' viewer, '.key extension', or 'web'
const viewer = () => (window.bsvkeyHost || location.href === 'about:srcdoc') ? 'bsvkey.com'
: location.protocol === 'chrome-extension:' ? 'extension' : 'web'
// storage that never throws: localStorage on the normal web, memory inside the sandbox
const store = {
get (k) { try { return localStorage.getItem(k) } catch { return k in mem ? mem[k] : null } },
set (k, v) { try { localStorage.setItem(k, v) } catch { mem[k] = String(v) } }
}
const getJSON = async (url) => { const r = await fetch(url); if (!r.ok) throw new Error('HTTP ' + r.status); return r.json() }
const price = async () => (await getJSON(WOC + '/exchangerate')).rate // USD per BSV
const height = async () => (await getJSON(WOC + '/chain/info')).blocks
const balance = async (addr) => { const b = await getJSON(WOC + '/address/' + addr + '/balance'); return b.confirmed + b.unconfirmed } // sat
const history = async (addr) => (await getJSON(WOC + '/address/' + addr + '/history')).slice(-50).reverse()
// many transactions in one request (WhatsOnChain bulk endpoint, 20 per call): fast and rate-limit friendly
const txs = async (ids) => {
const out = []
for (let i = 0; i < ids.length; i += 20) {
const r = await fetch(WOC + '/txs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ txids: ids.slice(i, i + 20) }) })
if (!r.ok) throw new Error('HTTP ' + r.status); out.push(...await r.json())
}
return out
}
const owner = async (name) => { const j = await getJSON(INDEX); const r = j.names[String(name).toLowerCase()]; return r ? r.owner : null }
// a link to bsvkey.com/pay: every wallet type (BRC-100, BSVKey wallet, QR for any wallet)
const payLink = ({ to, sats, label = '', memo = '', id = '' }) => 'https://bsvkey.com/pay?' + new URLSearchParams({ to, sats: String(sats), label, memo, id }).toString()
// call back once a payment of at least `sats` reaches `to` (address or name): instantly in the
// bsvkey.com viewer (it relays the pay tab), and by watching the chain everywhere else
const onPaid = async ({ to, sats, id = '', since = Date.now() }, cb) => {
let fired = false; const fire = (txid) => { if (!fired) { fired = true; cb(txid) } }
addEventListener('bsvkey:paid', (e) => { const d = e.detail || {}; if ((!id || d.id === id) && d.sats >= sats) fire(d.txid) })
const addr = /^[13]/.test(to) && to.length > 25 ? to : await owner(to)
const seen = new Set()
while (!fired) {
try {
const rows = ((await getJSON(WOC + '/address/' + addr + '/unconfirmed/history')).result || []).filter(r => !seen.has(r.tx_hash))
rows.forEach(r => seen.add(r.tx_hash))
for (const t of (rows.length ? await txs(rows.map(r => r.tx_hash)) : [])) {
if (t.time && t.time * 1000 < since - 120000) continue
const paid = t.vout.filter(o => (o.scriptPubKey.addresses || []).includes(addr)).reduce((s, o) => s + Math.round(o.value * 1e8), 0)
if (paid >= sats) { fire(t.txid); break }
}
} catch {}
await new Promise(r => setTimeout(r, 6000))
}
}
// notes people sent with bsvkey.com/pay?memo=... (guestbook, comments, orders). The pay page writes
// them as [OP_FALSE OP_RETURN "bsvkey:memo" <text>], so only real notes are read, never other data.
const pushes = (hex) => {
const out = []; let i = hex.indexOf('6a'); if (i < 0) return out; i += 2
while (i < hex.length) {
const op = parseInt(hex.substr(i, 2), 16); i += 2; let len = 0
if (op >= 1 && op <= 75) len = op
else if (op === 0x4c) { len = parseInt(hex.substr(i, 2), 16); i += 2 }
else if (op === 0x4d) { len = parseInt(hex.substr(i + 2, 2) + hex.substr(i, 2), 16); i += 4 }
else break
out.push(hex.substr(i, len * 2)); i += len * 2
}
return out
}
const utf8 = (h) => new TextDecoder('utf-8', { fatal: true }).decode(new Uint8Array((h.match(/../g) || []).map(x => parseInt(x, 16))))
const memos = async (addr, max = 40) => {
const hist = (await history(addr)).slice(0, max); const heights = Object.fromEntries(hist.map(h => [h.tx_hash, h.height]))
const out = []
for (const t of await txs(hist.map(h => h.tx_hash))) {
for (const o of t.vout) {
const hex = (o.scriptPubKey && o.scriptPubKey.hex) || ''
if (!/^(00)?6a/.test(hex)) continue
const p = pushes(hex.replace(/^00/, ''))
try { if (p.length >= 2 && utf8(p[0]) === 'bsvkey:memo') out.push({ txid: t.txid, height: heights[t.txid], text: utf8(p[1]) }) } catch {}
}
}
return out
}
return { viewer, store, getJSON, price, height, balance, history, txs, owner, payLink, onPaid, memos }
})()Tip or pay button that notices the payment
<!-- Tip / pay button: every wallet type, and it notices the payment by itself.
It is a real link (target="_blank"), so every viewer opens the pay page in a normal tab. -->
<a id="tip" class="btn" target="_blank" rel="noopener">Tip 5,000 sat</a>
<p id="tipMsg"></p>
<script>
// Needs the BK toolkit: paste it once, in its own <script> above this one.
{ // own scope, so several recipes can share one page
const TO = 'yourname' // your BSVKey name or a BSV address
const SATS = 5000
const tip = document.getElementById('tip')
const id = 'tip-' + Date.now()
tip.href = BK.payLink({ to: TO, sats: SATS, label: 'Tip for yourname', id })
tip.addEventListener('click', () => {
document.getElementById('tipMsg').textContent = 'Waiting for your payment…'
BK.onPaid({ to: TO, sats: SATS, id }, (txid) => {
document.getElementById('tipMsg').textContent = 'Thank you! Payment ' + txid.slice(0, 12) + '… received.'
})
}, { once: true })
}
</script>Live price, block height and balance
<!-- Live chain data: BSV price, block height and a balance, refreshed every minute. -->
<div class="card">
<div>BSV price: <b id="px">…</b></div>
<div>Block height: <b id="ht">…</b></div>
<div>Tip jar: <b id="bal">…</b></div>
</div>
<script>
// Needs the BK toolkit: paste it once, in its own <script> above this one.
{ // own scope, so several recipes can share one page
async function refresh () {
try {
const [usd, h, owner] = await Promise.all([BK.price(), BK.height(), BK.owner('yourname')])
document.getElementById('px').textContent = '$' + usd.toFixed(2)
document.getElementById('ht').textContent = h.toLocaleString()
const sat = await BK.balance(owner)
document.getElementById('bal').textContent = (sat / 1e8).toFixed(8) + ' BSV (≈ $' + (sat / 1e8 * usd).toFixed(2) + ')'
} catch (e) { document.getElementById('px').textContent = 'offline, retrying' }
}
refresh(); setInterval(refresh, 60000)
}
</script>On-chain guestbook (also comments, votes, orders)
<!-- On-chain guestbook: visitors pay a small amount with a note; the page reads the notes from the chain.
Tip: use a separate address just for the guestbook, so your other transactions don't mix in. -->
<input id="gbText" maxlength="200" placeholder="Leave a note">
<a id="gbSign" class="btn" target="_blank" rel="noopener">Sign (1,000 sat)</a>
<ul id="gbList"><li>Loading notes…</li></ul>
<script>
// Needs the BK toolkit: paste it once, in its own <script> above this one.
{ // own scope, so several recipes can share one page
const GUESTBOOK = '1YourGuestbookAddressHere' // a BSV address you control
const text = document.getElementById('gbText'); const sign = document.getElementById('gbSign')
const update = () => { sign.href = BK.payLink({ to: GUESTBOOK, sats: 1000, label: 'Guestbook', memo: text.value.trim() || 'Hello!' }) }
text.addEventListener('input', update); update()
BK.memos(GUESTBOOK, 30).then(notes => {
const ul = document.getElementById('gbList'); ul.innerHTML = ''
if (!notes.length) ul.innerHTML = '<li>No notes yet. Be the first!</li>'
for (const n of notes) { const li = document.createElement('li'); li.textContent = n.text; ul.appendChild(li) } // textContent: never inject visitor HTML
}).catch(() => { document.getElementById('gbList').innerHTML = '<li>Could not load notes right now.</li>' })
}
</script>Pay to unlock a bonus section
<!-- Pay-to-unlock. Honest note: the page is public on-chain, so anyone can read its source.
Use this for "thank you" extras, not for secrets. For truly private content, sell access from a
server (for example a BSVKey Paywall service) and link to it. -->
<div id="locked"><p>The bonus chapter unlocks after a 2,000 sat payment.</p><a id="buy" class="btn" target="_blank" rel="noopener">Unlock for 2,000 sat</a></div>
<div id="bonus" hidden><h2>Bonus chapter</h2><p>Thanks for supporting this site.</p></div>
<script>
// Needs the BK toolkit: paste it once, in its own <script> above this one.
{ // own scope, so several recipes can share one page
const TO = 'yourname'
const show = () => { document.getElementById('locked').hidden = true; document.getElementById('bonus').hidden = false }
if (BK.store.get('unlocked') === '1') show() // remembered on the normal web; inside a sandbox only for this visit
const id = 'unlock-' + Date.now()
const buy = document.getElementById('buy')
buy.href = BK.payLink({ to: TO, sats: 2000, label: 'Unlock bonus', id })
buy.addEventListener('click', () => BK.onPaid({ to: TO, sats: 2000, id }, () => { BK.store.set('unlocked', '1'); show() }), { once: true })
}
</script>Several pages in one file
<!-- A multi-page site in one file: each "page" is a section, the #hash picks which one shows.
(history.pushState is blocked in the bsvkey.com viewer; hash links work everywhere.) -->
<nav><a href="#home">Home</a> · <a href="#work">Work</a> · <a href="#contact">Contact</a></nav>
<section data-page="home"><h1>Home</h1><p>Welcome.</p></section>
<section data-page="work" hidden><h1>Work</h1><p>Things I made.</p></section>
<section data-page="contact" hidden><h1>Contact</h1><p><a href="mailto:you@example.com">you@example.com</a></p></section>
<script>
{ // own scope, so several recipes can share one page
function route () {
const page = (location.hash || '#home').slice(1)
document.querySelectorAll('[data-page]').forEach(s => { s.hidden = s.dataset.page !== page })
window.scrollTo(0, 0)
}
addEventListener('hashchange', route); route()
}
</script>Links that work in every viewer
<!-- Links that work in every viewer -->
<a href="https://othername.key/">othername.key</a> <!-- another BSVKey name: opens in the same viewer -->
<a href="https://bsvkey.com/#site=othername">othername on bsvkey.com</a> <!-- same, as a normal web link -->
<a href="https://example.com/" target="_blank" rel="noopener">A normal website</a> <!-- opens in a normal new tab -->
<a href="mailto:you@example.com">Email me</a> <!-- opens the visitor's mail app -->
<a href="https://bsvkey.com/market#/name/yourname" target="_blank" rel="noopener">Buy this name</a>
<a href="#work">Jump to a section</a> <!-- in-page links work too -->Images, and video with a fallback
<!-- Images: inline SVG or data: URIs work in every viewer (third-party image links only in the extension). -->
<svg width="120" height="120" viewBox="0 0 120 120" role="img" aria-label="logo">
<circle cx="60" cy="60" r="54" fill="#f7931a"/><text x="60" y="74" font-size="44" text-anchor="middle" fill="#1a1205" font-family="system-ui" font-weight="800">YN</text>
</svg>
<img alt="tiny inline image" width="48" height="48" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Crect width='4' height='4' fill='%2346c463'/%3E%3C/svg%3E">
<!-- Video: embeds play in the .key extension; the bsvkey.com viewer blocks embeds, so show a link there. -->
<div id="video"></div>
<script>
{ // own scope, so several recipes can share one page
const VIDEO_ID = 'dQw4w9WgXcQ' // a YouTube video id
const box = document.getElementById('video')
const isBsvkeyViewer = !!window.bsvkeyHost || location.href === 'about:srcdoc'
if (isBsvkeyViewer) box.innerHTML = '<a href="https://www.youtube.com/watch?v=' + VIDEO_ID + '" target="_blank" rel="noopener">▶ Watch the video</a>'
else box.innerHTML = '<iframe width="560" height="315" style="max-width:100%" src="https://www.youtube-nocookie.com/embed/' + VIDEO_ID + '" title="Video" allowfullscreen></iframe>'
}
</script>"This name is for sale" badge
<!-- Show whether a name is for sale, live from the BSVKey Name Market. -->
<p id="sale"></p>
<script>
{ // own scope, so several recipes can share one page
fetch('https://bsvkey.com/mkt/listings').then(r => r.json()).then(j => {
const l = j.listings.find(x => x.name === 'yourname' && x.status === 'live')
document.getElementById('sale').innerHTML = l
? 'This name is for sale: <b>' + (l.price / 1e8) + ' BSV</b>. <a href="https://bsvkey.com/market#/name/yourname" target="_blank" rel="noopener">Buy it</a>'
: ''
}).catch(() => {})
}
</script>Proof page: check a file against its fingerprint
<!-- Proof page: visitors drop a file and see whether it matches a fingerprint you published on-chain. -->
<p>Official SHA-256 of my document:<br><code id="want">paste-the-sha256-hex-here</code></p>
<input type="file" id="file"> <b id="verdict"></b>
<script>
{ // own scope, so several recipes can share one page
document.getElementById('file').onchange = async (e) => {
const f = e.target.files[0]; if (!f) return
const hash = await crypto.subtle.digest('SHA-256', await f.arrayBuffer())
const hex = [...new Uint8Array(hash)].map(b => b.toString(16).padStart(2, '0')).join('')
const ok = hex === document.getElementById('want').textContent.trim().toLowerCase()
document.getElementById('verdict').textContent = ok ? '✔ Matches. This is the original file.' : '✖ Does not match (' + hex.slice(0, 12) + '…)'
}
}
</script>Size and cost
Publishing costs a 5,000 sat fee plus the miner fee, about 1 sat per byte of your page.
| Page size | Approximate total | Good for |
|---|---|---|
| 10 KB | ~15,000 sat | A text page with inline SVG |
| 50 KB | ~56,000 sat | A rich page with the toolkit and a few small images |
| 100 KB | ~107,000 sat (about 0.001 BSV) | A small game or app, or a handful of compressed images |
Keep pages under about 100 KB so they load quickly. Use SVG for graphics, compress photos (WebP at small sizes), and don't repeat the toolkit. Republishing a change costs the same again, so test before you publish.
Make the most of your site
- Start from the starter page and keep the
<meta name="bsvkey:site" content="yourname">tag, so it's published to the right name. - Always include
<meta charset="utf-8">and a viewport tag, so text and phones look right. - Design for the bsvkey.com viewer first, then add extension-only extras behind
BK.viewer(). - Add a way to pay you: a tip button, a price, or a pay-with-note form.
- Show something live: your tip jar balance, the BSV price, the latest notes.
- Let visitors leave a mark with an on-chain guestbook or poll.
- Link your names together, so visitors can move between your sites inside the viewer.
- Use the default page in §11 to cover all your names at once, then give the important ones their own page.
- Share both addresses:
bsvkey.com/#site=yournamefor everyone andyourname.keyfor extension users. - Check it in both viewers after publishing (about 10 minutes), and on a phone.
- Keep a copy of your HTML file. On-chain is permanent, but editing starts from your own copy.
- Thinking of selling? A name with a good page is worth more. List it in the Name Market whenever you like.
Questions
Why can't my page load a script from a CDN?
Outside scripts could change after you publish, and they could run anything inside the viewer. Paste the library into your page instead; then it's on-chain and can't change.
Why does my page forget things between visits?
Sandboxed pages get no storage, so one site can't track visitors or read another site's data. Keep state in memory for the visit, put choices in the #hash, or record them on-chain with a pay note.
Can my page use the visitor's wallet?
Not directly, on purpose: a page you didn't write must never touch your wallet. Payments go through bsvkey.com/pay in its own tab, where the visitor chooses a wallet and approves the payment.
Is pay-to-unlock secure?
Only for low-stakes extras. The page is public on-chain, so anyone can read its source. For anything private, sell access from a server and link to it.
Why do images from other sites show in the extension but not on bsvkey.com?
The bsvkey.com viewer shares a website with the wallet app, so it allows only images that are part of the page (inline SVG, data URIs) or from bsvkey.com. The extension runs on its own, so it can allow more.
My links don't open. What's wrong?
Use full addresses (https://…). Links to other names open in the viewer; other websites open in a normal new tab. Relative links like page2.html don't exist on-chain: use #sections instead.
How do I update my site?
Publish the new version from the same wallet and name in §11. The newest page wins after about 10 minutes. Old versions stay on-chain, so never publish anything you'd want to take back.
How do I test before publishing?
Open your file in a normal browser first. Then check the bsvkey.com viewer's limits: no outside scripts, styles or images, no storage. The toolkit's BK.viewer() and BK.store make the same code work in every case.