Auth Standard | BNRP-IP-10

Sign in with
Bitcoin

PSBT challenge authentication for .btc names. No passwords. No OAuth. No ETH. Own your identity on Bitcoin.

01

How it works

Three steps. No passwords, no cookies, no server-side secret storage. The only thing that proves you own a .btc name is your private key.

Step 01

Generate nonce challenge

Your site calls createChallenge(). A random 16-byte nonce is generated and embedded in a canonical message with your domain, the user's .btc name, and a timestamp.

Step 02

Wallet signs with BIP-322

The user's wallet (UniSat, Xverse, etc.) signs the message using BIP-322 simple signing. No transaction is broadcast. No fees. Just a Schnorr signature over the challenge text.

Step 03

Server verifies ownership

Your backend calls verifySignature(). It checks the Schnorr signature against the P2TR pubkey, then confirms the address matches the on-chain owner of the .btc name via BNRP.

02

Live demo

Enter a .btc name, generate a challenge, and sign it with your wallet. The signature is verified locally using the SIWB library.

SIWB Demo live
No compatible wallet detected.
  1. Install UniSat Wallet or Xverse browser extension.
  2. Create or import a wallet with a P2TR (Taproot) address.
  3. Paste your bc1p... address in the field above and click Generate Challenge.
  4. Then click Connect Wallet & Sign to trigger the wallet's sign prompt.
03

Message Format

The canonical SIWB message. Plain text. Human-readable. Every field is required except Expiration Time.

SIWB canonical message — BNRP-IP-10
btcnative.name wants you to sign in with your Bitcoin name Name: satoshi.btc Address: bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0 URI: https://btcnative.name Version: 1 Nonce: a3f9b2c1d0e4f5a6b7c8d9e0f1a2b3c4 Issued At: 2026-05-16T21:00:00.000Z I accept the BTC Native Terms of Service. This request will not trigger a blockchain transaction or cost any gas.
Field Description Required
domain The domain requesting authentication. Bound to the challenge. Prevents cross-site replay attacks. Yes
Name The .btc name the user claims to own. Server verifies this resolves to the signing address. Yes
Address Bitcoin P2TR address (bc1p...). The Schnorr signature is verified against the x-only pubkey extracted from this address. Yes
URI Full URI of the requesting resource. RFC-3986 compliant. Yes
Version SIWB spec version. Currently 1. Yes
Nonce 32-character hex random nonce (16 bytes). Prevents replay attacks. Single-use server-side. Yes
Issued At ISO 8601 timestamp. Challenge expires 10 minutes after issuance by default. Yes
Expiration Time Optional explicit expiry. ISO 8601. If omitted, server enforces a 10-minute window from Issued At. No
Statement Human-readable terms. Appears after the blank line. Shown verbatim in wallet signing prompt. Yes
04

Add to your site

Drop in the script tag. The library is under 15KB, pure client-side, no dependencies.

HTML
<!-- UMD bundle, exposes window.SIWB --> <script src="https://btcnative.name/siwb.js"></script> <!-- Or as ES module --> <script type="module"> import { createChallenge, verifySignature } from 'https://btcnative.name/siwb.js'; </script>
client.js
// 1. Generate challenge const { message, nonce } = SIWB.createChallenge({ domain: window.location.host, address: userAddress, btcName: 'satoshi.btc', uri: window.location.href }); // 2. Request wallet signature const sig = await window.unisat.signMessage( message.toMessage() ); // 3. Send to your server await fetch('/v1/siwb/verify', { method: 'POST', body: JSON.stringify({ message: message.toMessage(), signature: sig, address: userAddress }) });
Always verify the signature server-side. Client-side verification is for UX feedback only. The server must also confirm the signing address owns the claimed .btc name by resolving it through the BNRP API.
05

Server verification

Use the same library in Node.js or a Cloudflare Worker. Web Crypto API is available in both environments.

worker.js (Cloudflare)
import { verifySignature, SIWBMessage } from '@btc-native/bnrp/siwb'; export default { async fetch(req) { const { message, signature, address } = await req.json(); const msg = SIWBMessage.fromMessage(message); // 1. Verify Schnorr signature const result = await verifySignature({ message: msg, signature, address }); if (!result.ok) return err(result.error); // 2. Check name resolves to address const owner = await resolveOwner(msg.btcName); if (owner !== msg.address) return err('Name mismatch'); // 3. Check nonce not yet used await markNonceUsed(msg.nonce); return Response.json({ ok: true, name: msg.btcName, owner: address }); } };
server.js (Node 18+)
import express from 'express'; import { verifySignature, SIWBMessage } from '@btc-native/bnrp/siwb'; app.post('/v1/siwb/verify', async (req, res) => { const { message, signature, address } = req.body; const msg = SIWBMessage.fromMessage(message); // Check expiry const issued = new Date(msg.issuedAt); if (Date.now() - issued > 600_000) return res.status(401).json({ ok: false, error: 'Expired' }); const result = await verifySignature({ message: msg, signature, address }); if (!result.ok) return res.status(401).json(result); res.json({ ok: true, name: msg.btcName }); });
06

Endpoint spec

The BNRP API exposes two endpoints implementing the server-side SIWB flow. Base URL: https://api.bnrp.name

Endpoint Description Body / Response
POST /v1/siwb/challenge Generate a signed challenge for a given address and .btc name. Returns a fresh nonce and canonical message text. Challenge expires in 10 minutes. Body:
{ domain, address, btcName }

Response:
{ message, nonce, expiresAt }
POST /v1/siwb/verify Verify a BIP-322 Schnorr signature against the challenge message and confirm the address matches the on-chain .btc name owner via BNRP resolution. Nonce is invalidated after use. Body:
{ message, signature, address }

Response:
{ ok, name, owner }
All BNRP API endpoints are CORS-enabled and rate-limited per IP. For production auth flows, implement nonce deduplication in your own persistent store (KV, Redis) to prevent replay attacks.