SkateSpots/ESCROW.md
SkateSpots 26e951710e feat(marketplace): Cashu escrow core (time-locked P2PK) + design
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.
2026-09-26 21:01:31 +07:00

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. 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", "<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.