# 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. See `NIP.md` → Profile Metadata Extensions. Nostr x-only keys are accepted too (normalised with an `02` prefix, matching coco's own `getPublicKeyHex`). ## Flow 1. Seller advertises `cashu_escrow` (set when they touch their Cashu wallet). 2. 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)` → pay `request` via NWC/WebLN/Cashu melt - `mintProofs(amount, quote, { p2pk: { pubkey, locktime, refundKeys:[buyer] } })` - encode → the `cashu_token` on the `31872` confirmation 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. 3. Seller sees the escrowed token on the request and redeems (`wallet.receive`). 4. If not delivered by `locktime`, the buyer refunds. ## Protocol The escrow token rides the Attestations NIP's existing optional `["cashu_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 injectable `createEscrowToken` mint 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` — `31872` request carries `cashu_token`. - `NIP.md` — profile field + protocol notes.