aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build.js4
-rw-r--r--static/docs/index.html172
2 files changed, 176 insertions, 0 deletions
diff --git a/build.js b/build.js
index bac1079..5a279c4 100644
--- a/build.js
+++ b/build.js
@@ -90,6 +90,8 @@ self.addEventListener('fetch', (e) => {
// copy, the player stalled and failed), and video segments would fill the
// cache with what's already been watched.
if (u.pathname.startsWith('/live/') || u.pathname.startsWith('/vmedia/hls')) return;
+ // The developer docs are their own page, not the app: never the offline shell.
+ if (u.pathname === '/docs' || u.pathname.startsWith('/docs/')) return;
if (req.mode === 'navigate') {
// A note page comes back with its thread rendered into it (the server's
@@ -535,6 +537,8 @@ if (import.meta.main) {
// ...and the default profile covers, full size and the wizard's preview copies
for (const d of ['punks', 'punks-sm', 'covers', 'covers-sm'])
for (const f of readdirSync('static/' + d)) await Bun.write(`dist/${d}/${f}`, Bun.file(`static/${d}/${f}`));
+ // the developer docs, a plain page at /docs/
+ for (const f of readdirSync('static/docs')) await Bun.write(`dist/docs/${f}`, Bun.file(`static/docs/${f}`));
const kb = (Buffer.byteLength(html) / 1024).toFixed(0);
const jkb = (Buffer.byteLength(rawJs) / 1024).toFixed(0);
diff --git a/static/docs/index.html b/static/docs/index.html
new file mode 100644
index 0000000..631faf9
--- /dev/null
+++ b/static/docs/index.html
@@ -0,0 +1,172 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="color-scheme" content="light dark">
+<title>Coinos for developers</title>
+<meta name="description" content="How to build on coinos: Lightning addresses, Nostr Wallet Connect, the coinos REST API, and testing on mutinynet.">
+<link rel="icon" href="/icon.svg" type="image/svg+xml">
+<style>
+@font-face { font-family: 'Public Sans'; src: url('/PublicSans.woff2') format('woff2'); font-weight: 100 900; font-display: swap; }
+:root {
+ --bg: #fafaf9; --fg: #1c1917; --muted: #57534e; --faint: #a8a29e;
+ --card: #ffffff; --line: #e7e5e4; --code: #f5f5f4; --accent: #f7931a; --link: #c2410c;
+}
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ --bg: #0c0a09; --fg: #f5f5f4; --muted: #a8a29e; --faint: #57534e;
+ --card: #1c1917; --line: #292524; --code: #1c1917; --link: #fb923c;
+ }
+}
+:root[data-theme="dark"] {
+ --bg: #0c0a09; --fg: #f5f5f4; --muted: #a8a29e; --faint: #57534e;
+ --card: #1c1917; --line: #292524; --code: #1c1917; --link: #fb923c;
+}
+* { box-sizing: border-box; }
+body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.6 'Public Sans', system-ui, sans-serif; }
+main { max-width: 760px; margin: 0 auto; padding: 32px 16px 80px; }
+header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
+header img { width: 36px; height: 36px; }
+header a { color: var(--fg); text-decoration: none; font-weight: 700; font-size: 20px; }
+h1 { font-size: 32px; line-height: 1.2; margin: 24px 0 8px; }
+h2 { font-size: 22px; margin: 48px 0 8px; padding-top: 8px; border-top: 1px solid var(--line); }
+h3 { font-size: 17px; margin: 28px 0 6px; }
+p, li { color: var(--fg); }
+.lede { color: var(--muted); font-size: 18px; }
+a { color: var(--link); }
+code { font: 13.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--code); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; overflow-wrap: anywhere; }
+pre { background: var(--code); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; overflow-x: auto; }
+pre code { border: 0; padding: 0; background: none; overflow-wrap: normal; }
+nav.toc { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 12px 18px; margin: 24px 0; }
+nav.toc ol { margin: 0; padding-left: 20px; }
+table { width: 100%; border-collapse: collapse; font-size: 14.5px; margin: 8px 0; }
+th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
+th { color: var(--muted); font-weight: 600; }
+.note { border-left: 3px solid var(--accent); background: var(--card); padding: 10px 14px; border-radius: 0 8px 8px 0; margin: 16px 0; }
+.wrap { overflow-x: auto; }
+footer { margin-top: 64px; color: var(--faint); font-size: 14px; }
+</style>
+</head>
+<body>
+<main>
+<header><img src="/icon.svg" alt=""><a href="/">coinos</a></header>
+
+<h1>Building on coinos</h1>
+<p class="lede">There are two coinos wallets, and they connect to other software differently. The new one (v3, this site) keeps your keys on your device. The original one at coinos.io is a hosted account with a REST API.</p>
+
+<nav class="toc"><ol>
+ <li><a href="#which">Which one should I build against?</a></li>
+ <li><a href="#addresses">Receiving: Lightning addresses, LNURL, BIP-353</a></li>
+ <li><a href="#nwc">Nostr Wallet Connect (v3)</a></li>
+ <li><a href="#rest">The coinos REST API (hosted accounts)</a></li>
+ <li><a href="#migration">Moving a username to v3</a></li>
+ <li><a href="#testing">Testing without real money</a></li>
+ <li><a href="#source">Source code</a></li>
+</ol></nav>
+
+<h2 id="which">Which one should I build against?</h2>
+<div class="wrap"><table>
+ <tr><th></th><th>coinos v3 (v3.coinos.io)</th><th>coinos.io hosted accounts</th></tr>
+ <tr><td>Custody</td><td>Self-custodial. The wallet runs in the browser or the app; keys never leave the device.</td><td>Custodial. coinos holds the balance.</td></tr>
+ <tr><td>Programmatic access</td><td>Nostr Wallet Connect (NIP-47), with spending limits you set per connection</td><td>REST API with a bearer token, plus a websocket</td></tr>
+ <tr><td>Receive</td><td>Lightning address, LNURL-pay, BOLT12, Ark, on-chain</td><td>Lightning, on-chain, Liquid, internal</td></tr>
+ <tr><td>Send on-chain from code</td><td>No</td><td>Yes: <code>/bitcoin/send</code></td></tr>
+ <tr><td>Good for</td><td>Apps that pay or charge a user's own wallet: zaps, tips, small checkouts</td><td>Services that run unattended: shops, bots, swaps, POS hardware</td></tr>
+</table></div>
+<p>v3 has no server-side API or token. There's no account on our side for one to reach. If your service has to run around the clock without anyone's wallet being open, use a hosted coinos.io account and the REST API.</p>
+
+<h2 id="addresses">Receiving: Lightning addresses, LNURL, BIP-353</h2>
+<p>Every coinos name <code>[email protected]</code> is a Lightning address, and it works the same whether the name belongs to a v3 wallet or a hosted account.</p>
+<h3>LNURL-pay (LUD-06/16)</h3>
+<pre><code>GET https://coinos.io/.well-known/lnurlp/&lt;name&gt;</code></pre>
+<p>This returns a standard <code>payRequest</code>. Call its <code>callback</code> with <code>?amount=&lt;millisats&gt;</code> to get a BOLT11 invoice. Comments up to 200 characters are accepted (LUD-12). <code>allowsNostr</code> is true, so NIP-57 zaps work and receipts are published.</p>
+<h3>Nostr identifiers (NIP-05)</h3>
+<pre><code>GET https://coinos.io/.well-known/nostr.json?name=&lt;name&gt;</code></pre>
+<h3>BIP-353 (₿[email protected])</h3>
+<p>v3 names publish a DNSSEC-signed payment instruction:</p>
+<pre><code>dig TXT &lt;name&gt;.user._bitcoin-payment.coinos.io</code></pre>
+<p>The record is a <code>bitcoin:</code> URI carrying an Ark address and a BOLT12 offer (<code>lno=</code>). Wallets that speak BIP-353 can pay it with no web request at all.</p>
+
+<h2 id="nwc">Nostr Wallet Connect (v3)</h2>
+<p>NWC lets your app ask a user's v3 wallet to pay or create invoices over nostr. Your app never sees the wallet's keys. The user decides how much it may spend, and can cut it off at any time.</p>
+<h3>Getting a connection string</h3>
+<ol>
+ <li>In coinos, open <strong>Settings → Nostr → Nostr Wallet Connect</strong>.</li>
+ <li><strong>Add a connection</strong>: give it a name, a maximum per payment, and a budget (per day, week or month).</li>
+ <li>Copy the <code>nostr+walletconnect://…</code> string into your app.</li>
+</ol>
+<h3>Supported methods</h3>
+<div class="wrap"><table>
+ <tr><th>Method</th><th>Notes</th></tr>
+ <tr><td><code>get_info</code></td><td>Lists the methods below.</td></tr>
+ <tr><td><code>get_balance</code></td><td>Spendable balance, in millisats.</td></tr>
+ <tr><td><code>get_budget</code></td><td>What's left of this connection's budget, and when it renews.</td></tr>
+ <tr><td><code>pay_invoice</code></td><td>BOLT11. Refused past the per-payment cap or the budget.</td></tr>
+ <tr><td><code>make_invoice</code></td><td>A BOLT11 invoice paid into the wallet.</td></tr>
+ <tr><td><code>lookup_invoice</code></td><td>By <code>payment_hash</code> or <code>invoice</code>; tells you whether it's settled.</td></tr>
+ <tr><td><code>list_transactions</code></td><td>Recent payments in and out.</td></tr>
+</table></div>
+<p>NIP-47 has no on-chain method, and coinos doesn't add one. A revoked connection answers <code>UNAUTHORIZED</code>.</p>
+<div class="note"><strong>Who answers.</strong> Requests are answered by the user's wallet itself, not by a coinos server. It answers while coinos is open, and in the background too if the user turned on <em>Answer while closed</em>, which uses push notifications to wake it. Expect a few seconds of latency when it has to wake. Don't rely on it like an always-on server.</div>
+<p>Requests and responses go through <code>wss://relay.coinos.io</code>, plus any relays in the connection string.</p>
+
+<h2 id="rest">The coinos REST API (hosted accounts)</h2>
+<p>The REST API at <code>https://coinos.io/api</code> keeps working for hosted accounts. The full reference is at <a href="https://coinos.io/docs">coinos.io/docs</a>. A short tour:</p>
+<pre><code>BASE=https://coinos.io/api
+
+# an account and a token (or copy a token from the coinos.io settings page)
+curl -X POST $BASE/register -H 'content-type: application/json' \
+ -d '{"user":{"username":"myshop","password":"…"}}'
+curl -X POST $BASE/login -H 'content-type: application/json' \
+ -d '{"username":"myshop","password":"…"}' # → {"token": "…"}
+
+AUTH="authorization: Bearer $TOKEN"
+curl $BASE/me -H "$AUTH" # balance and profile
+
+# receive: a Lightning invoice (type "bitcoin" or "liquid" for an address)
+curl -X POST $BASE/invoice -H "$AUTH" -H 'content-type: application/json' \
+ -d '{"invoice":{"amount":1000,"type":"lightning"}}' # → {"hash", "text": "lnbc…", …}
+curl $BASE/invoice/&lt;hash&gt; # received / pending
+
+# pay a Lightning invoice
+curl -X POST $BASE/payments -H "$AUTH" -H 'content-type: application/json' \
+ -d '{"payreq":"lnbc…"}'
+
+# pay a Lightning address
+curl -X POST $BASE/send/[email protected]/1000 -H "$AUTH"
+
+# on-chain: estimate, then send
+curl -X POST $BASE/bitcoin/fee -H "$AUTH" -H 'content-type: application/json' -d '{"address":"bc1…","amount":50000}'
+curl -X POST $BASE/bitcoin/send -H "$AUTH" -H 'content-type: application/json' -d '{"address":"bc1…","amount":50000}'
+
+curl "$BASE/payments" -H "$AUTH" # history</code></pre>
+<p>Don't poll for payments: connect to <code>wss://coinos.io/ws</code>, send <code>{"type":"login","data":"&lt;token&gt;"}</code>, and receive payment events as they happen. Exact request fields can change; <a href="https://coinos.io/docs">coinos.io/docs</a> is authoritative.</p>
+<div class="note">When v3 takes over the coinos.io homepage, the hosted wallet and its API move to <code>v2.coinos.io</code>. We'll announce the date ahead of time. Until then nothing changes, and <code>https://coinos.io/api</code> is the right base URL. <code>v3.coinos.io</code> has no equivalent API.</div>
+
+<h2 id="migration">Moving a username to v3</h2>
+<p>A hosted account's username can move to a v3 wallet. After that, <strong>new payments to the hosted account</strong> (Lightning, BOLT12, on-chain, Liquid) are forwarded to the v3 wallet automatically. Payments between coinos accounts aren't forwarded. Your API token keeps working, but the money won't stay in the hosted balance. So if your code receives into a hosted account and then spends from it (a swap or a payout), <strong>don't migrate that account's username</strong>. Use a separate name for your v3 wallet.</p>
+
+<h2 id="testing">Testing without real money</h2>
+<p>Both wallets have a copy on <a href="https://mutinynet.com">mutinynet</a>, a test network with 30-second blocks. Its coins are free and worthless.</p>
+<div class="wrap"><table>
+ <tr><th>What</th><th>Where</th></tr>
+ <tr><td>REST API on mutinynet</td><td><code>https://staging.coinos.io/api</code>: the same endpoints as coinos.io/api, with separate accounts. Websocket: <code>wss://staging.coinos.io/api/ws</code></td></tr>
+ <tr><td>v3 wallet on mutinynet</td><td><a href="https://staging.coinos.io">staging.coinos.io</a>: the same app, defaulting to mutinynet, with its own Ark server and Lightning node</td></tr>
+ <tr><td>Test coins</td><td><a href="https://faucet.mutinynet.com">faucet.mutinynet.com</a>: on-chain coins, or pay a mutinynet Lightning invoice</td></tr>
+ <tr><td>Block explorer</td><td><a href="https://mutinynet.com">mutinynet.com</a></td></tr>
+</table></div>
+<p>Switch your base URL to <code>https://staging.coinos.io/api</code> and register a fresh account there. Mutinynet addresses start with <code>tb1</code>, and its invoices with <code>lntbs</code>. Liquid is switched off on staging. The test server is reset now and then, so don't keep anything there you'd miss.</p>
+
+<h2 id="source">Source code</h2>
+<ul>
+ <li>coinos v3 (this app): <a href="https://github.com/coinos/coinosv3">github.com/coinos/coinosv3</a></li>
+ <li>The hosted wallet's server: <a href="https://github.com/coinos/coinos-server">github.com/coinos/coinos-server</a> (its routes are in <code>index.ts</code>)</li>
+ <li>Mirrors: <a href="https://git.coinos.io">git.coinos.io</a></li>
+</ul>
+<p>Questions, or something here that's wrong? Message <a href="/adam">[email protected]</a> on nostr.</p>
+
+<footer>coinos</footer>
+</main>
+</body>
+</html>