Quickstart
Three things to do — in order, once. After this, sending and receiving are one click.
1. Register your device
Visit /app in a browser, enter your Bitcoin address, pick your wallet (UniSat, Xverse, Leather — or paste a signature from any signmessage-capable wallet), and sign the binding statement. The browser stores a 32-byte X25519 secret in IndexedDB and publishes a public kind-30078 event to four Nostr relays.
You never do this step again for this address in this browser.
2. Send a message
On /app, click + new and paste the recipient's
Bitcoin address. The thread opens. Type a message or attach files and press
send. The app looks up the recipient on Nostr, verifies the BIP-322 binding
on their device record, seals the envelope, publishes a gift-wrap so the
recipient's inbox auto-delivers, and exposes a share URL / .lock / QR on the
resulting bubble so you can also hand the envelope off out-of-band any time.
Every sent message is a sealed vault by definition — same crypto, same identity binding, same envelope structure. The URL encodes the full envelope in its fragment, so nothing touches a server.
https://oc-lock-web.vercel.app/unlock#eyJ2Ijoy…
Share the URL through any channel: Signal, email, paper QR code, carrier pigeon.
3. Receive a message
Open the share URL. The app:
- Recomputes the envelope id and verifies the sender's BIP-322 signature.
- Looks up your local device key by
device_id. - Derives
shared = X25519(device_sk, eph_pk)from the recipient entry. - Unwraps the content key under
HKDF(shared, salt=nonce_ct). - Decrypts the payload with AES-256-GCM.
Total on-screen time: < 3 seconds.
4. Hand off an envelope
Every sent bubble exposes three affordances: copy url, download .lock, show qr. Same envelope, three ways to deliver it out-of-band if the Nostr auto-delivery isn't convenient (counterpart isn't on Lock yet, in-person hand-off, air-gapped recipient). See chat transport for the full delivery model and security implications for the trust model.
Using the SDK
If you want to embed OC Lock in another app — Node, a browser extension, a Nostr
client — the three packages are published from the
oc-packages monorepo.
yarn add @orangecheck/lock-core @orangecheck/lock-crypto @orangecheck/lock-device
Seal:
import { seal } from '@orangecheck/lock-core/seal';
import { utf8Encode } from '@orangecheck/lock-crypto';
const envelope = await seal({
payload: utf8Encode('hi bob'),
sender: {
address: 'bc1qalice…',
signMessage: async (msg) => walletSignBIP322(msg),
},
recipients: [{ address: 'bc1qbob…', device_id: '…', device_pk: '…' }],
});
Unseal:
import { unseal } from '@orangecheck/lock-core/seal';
const result = await unseal({
envelope,
device: { device_id: '…', secretKey: localDeviceSecret },
verifyBip322: async (msg, sig, addr) => myVerifier(msg, sig, addr),
});
const plaintext = new TextDecoder().decode(result.payload);
Device-key lifecycle:
import {
buildBindingStatement,
finalizeDeviceEvent,
generateDeviceKey,
} from '@orangecheck/lock-device';
const kp = generateDeviceKey();
const statement = buildBindingStatement({
address: btcAddress,
device_pk: kp.device_pk,
device_id: kp.device_id,
created_at: kp.created_at,
});
const signature = await wallet.signBIP322(statement);
const event = finalizeDeviceEvent({
deviceSk: kp.device_sk,
address: btcAddress,
device_id: kp.device_id,
device_pk: kp.device_pk,
bindingStatement: statement,
bindingSigBase64: signature,
});
// publish `event` to Nostr relays of your choice
Layered with OrangeCheck
Need a sybil filter on your inbox? Gate unseal on an OrangeCheck check:
import { unseal } from '@orangecheck/lock-core/seal';
import { check } from '@orangecheck/sdk';
const ok = await check({
addr: envelope.from.address,
minSats: 100_000,
minDays: 30,
});
if (!ok.ok) throw new Error('stake too low');
const out = await unseal({ envelope, device, verifyBip322 });
Next
- Protocol walkthrough — narrative version, flow diagrams.
- Specification — normative rules, canonicalization, error codes.
- FAQ — common questions.