Real, oracle-free escrow built on Cashu P2PK with a locktime + refund key:
buyer locks funds to the seller; seller redeems on delivery; the buyer can
reclaim after the deadline. Locked (not plain) tokens are what make the
cashu_token on a public 31872 safe.
- src/lib/cashuEscrow.ts: pubkey normalisation (x-only -> compressed), P2PK
policy builder, offline token inspection (amount/mint/locktime/refund), and
an injectable createEscrowToken mint flow (quote -> pay -> mintProofs{p2pk})
- 15 unit tests incl. the mint orchestration against a fake wallet
- Protocol: advertise a 'cashu_escrow' pubkey in kind 0 (NIP.md); token rides
the existing 31872 cashu_token tag
- ESCROW.md: design, threat model, status, limitations
- AGENTS.md: project map + docs index updated
Wallet UI (cashu-ts + payment rail + profile publish) is native-only and still
needs on-device verification.
4.1 KiB
Cashu Escrow (marketplace)
Status: core implemented & unit-tested; wallet UI is native-only and awaits on-device verification. See "Status" at the bottom before trusting it with real money.
Goal
Let a buyer and seller close a marketplace deal where the buyer's funds are committed but recoverable, so neither side has to trust the other blindly — without a custodial third party and without an always-online oracle.
Model — time-locked P2PK
Cashu P2PK (NUT-11) supports a locktime and refund keys. We use
exactly that:
buyer ──locks funds to──▶ seller pubkey
locktime = now + N days
refund = buyer pubkey
- Seller delivered → redeems with their key (any time).
- Seller vanished / deal fell through → after
locktime, the buyer reclaims with their refund key.
This is real escrow (both sides protected) with no witness/oracle — the only thing "release on attestation" needs is the locktime, which the mint enforces.
Contrast with the naive option (lock to the seller, no expiry): that's prepayment, not escrow — the seller can take the money and never deliver — so we deliberately don't do it.
Keys
Cashu P2PK needs a secp256k1 key the wallet can sign a witness with. The
Nostr signer (Amber / NIP-07) can't produce a bare schnorr signature, so we use
the Cashu wallet's own keyring (coco.keyring), not the Nostr key.
Each user advertises one escrow pubkey for others to lock to:
- kind 0 content field
cashu_escrow— 33-byte compressed hex (02…), derived from the Cashu keyring. SeeNIP.md→ Profile Metadata Extensions. Nostr x-only keys are accepted too (normalised with an02prefix, matching coco's owngetPublicKeyHex).
Flow
- Seller advertises
cashu_escrow(set when they touch their Cashu wallet). - Buyer reads the seller's pubkey, then funds escrow by paying the mint's
Lightning invoice and mints fresh proofs locked to the seller:
createMintQuote(amount)→ payrequestvia NWC/WebLN/Cashu meltmintProofs(amount, quote, { p2pk: { pubkey, locktime, refundKeys:[buyer] } })- encode → the
cashu_tokenon the31872confirmation request. - Funding by mint (not by spending the coco balance) keeps this independent of the coco wallet's proof state — no state bridging, no counter desync.
- Seller sees the escrowed token on the request and redeems (
wallet.receive). - If not delivered by
locktime, the buyer refunds.
Protocol
The escrow token rides the Attestations NIP's existing optional
["cashu_token", "<token>"] tag on the kind 31872 request (NIP.md → Trade
Attestations). The lock is what makes publishing it safe: a plain (unlocked)
token in a public event is spendable by anyone who sees it.
Threat model / limitations
- Mint trust: P2PK keeps the mint from spending your funds, but you still trust the mint to be solvent and redeemable. Use a reputable mint.
- No delivery proof on-chain: "delivered" is the seller's word; the buyer's
protection is the locktime refund, and the marketplace reputation system
(
31871) records the outcome. - Locktime is unilateral: the buyer can always refund after the deadline even if the seller did deliver but was slow to redeem — so redemption should happen promptly, and lock windows should be generous.
- Native only: the app's Cashu wallet is Capacitor-only, so escrow is too.
Status
- ✅ Core (
src/lib/cashuEscrow.ts, unit-tested): pubkey normalisation,escrowP2PKOptions, offline token inspection/parseEscrowToken, locktime/refund policy, and the injectablecreateEscrowTokenmint flow. - 🟡 Wallet UI (native-only): needs a real mint + Android device to verify; do not treat as production until exercised end-to-end on-device.
Files
src/lib/cashuEscrow.ts(+.test.ts) — pure escrow policy & inspection.src/hooks/wallet/useCashu.ts— Cashu wallet (native-only), keyring source.src/hooks/useTradeAttestations.ts—31872request carriescashu_token.NIP.md— profile field + protocol notes.