Learn-skills.dev nostr-marketplace-builder
Build Nostr marketplace applications using NIP-15 stalls, products, auctions, and NIP-69 P2P orders. Use when creating e-commerce on Nostr, building product listings, setting up stalls with shipping zones, implementing auction bidding systems, constructing checkout flows with NIP-04 DMs, creating P2P trading orders, or generating marketplace UI configurations. Covers kind:30017 stalls, kind:30018 products, kind:30020 auctions, kind:1021 bids, kind:1022 bid confirmations, kind:30019 marketplace UI, and kind:38383 P2P orders.
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/accolver/skill-maker/nostr-marketplace-builder" ~/.claude/skills/neversight-learn-skills-dev-nostr-marketplace-builder && rm -rf "$T"
data/skills-md/accolver/skill-maker/nostr-marketplace-builder/SKILL.mdNostr Marketplace Builder
Overview
Construct correct Nostr marketplace events for commerce applications. This skill handles the non-obvious parts: stall/product relationships, shipping cost calculations, auction lifecycle rules, checkout message sequencing via NIP-04, and P2P order tag structures from NIP-69.
When to Use
- Creating a merchant stall with shipping zones (kind:30017)
- Listing products with categories and specs (kind:30018)
- Setting up auctions with bidding (kind:30020, kind:1021, kind:1022)
- Building checkout flows (NIP-04 order → payment → status)
- Creating P2P buy/sell orders for bitcoin trading (NIP-69 kind:38383)
- Configuring a custom marketplace UI (kind:30019)
- Calculating shipping costs across zones and products
Do NOT use when:
- Building generic Nostr events (use nostr-event-builder)
- Implementing relay WebSocket logic
- Working with NIP-19 encoding/decoding
Workflow
1. Identify What to Build
| Intent | Kind(s) | NIP |
|---|---|---|
| Create/update a merchant stall | 30017 | NIP-15 |
| List a product for sale | 30018 | NIP-15 |
| Configure marketplace UI | 30019 | NIP-15 |
| Create an auction listing | 30020 | NIP-15 |
| Place a bid on an auction | 1021 | NIP-15 |
| Confirm/reject a bid | 1022 | NIP-15 |
| Checkout (order/pay/status) | NIP-04 DMs | NIP-15 |
| P2P buy/sell order | 38383 | NIP-69 |
2. Build the Event
Stall (Kind:30017) — Addressable
The stall is the merchant's store. Products belong to stalls. Shipping zones are defined at the stall level with base costs.
{ "kind": 30017, "tags": [["d", "my-stall-001"]], "content": "{\"id\":\"my-stall-001\",\"name\":\"Satoshi's Electronics\",\"description\":\"Quality electronics for sats\",\"currency\":\"USD\",\"shipping\":[{\"id\":\"us-domestic\",\"name\":\"US Domestic\",\"cost\":5.99,\"regions\":[\"US\"]},{\"id\":\"worldwide\",\"name\":\"Worldwide\",\"cost\":15.00,\"regions\":[\"EU\",\"AS\",\"SA\"]}]}" }
Rules:
tag value MUST equal thed
in content JSONid
should be a UUID or descriptive slug — sequential IDs (id
,0
,1
) are discouraged2
is a string (e.g.,currency
,"USD"
,"BTC"
)"EUR"
array defines zones; customer must choose exactly one zone at checkoutshipping- Each zone has a base
in the stall's currencycost
Product (Kind:30018) — Addressable
Products belong to a stall via
stall_id. They can add per-product shipping
costs on top of the stall's base shipping cost.
{ "kind": 30018, "tags": [ ["d", "product-xyz-789"], ["t", "electronics"], ["t", "gadgets"] ], "content": "{\"id\":\"product-xyz-789\",\"stall_id\":\"my-stall-001\",\"name\":\"Lightning Node Kit\",\"description\":\"Pre-configured Bitcoin node\",\"images\":[\"https://example.com/node.jpg\"],\"currency\":\"USD\",\"price\":299.99,\"quantity\":25,\"specs\":[[\"processor\",\"ARM Cortex-A72\"],[\"storage\",\"1TB SSD\"],[\"connectivity\",\"Ethernet + WiFi\"]],\"shipping\":[{\"id\":\"us-domestic\",\"cost\":10.00},{\"id\":\"worldwide\",\"cost\":25.00}]}" }
Rules:
tag value MUST equal thed
in content JSONid
MUST reference an existing stall'sstall_idid
: integer for limited stock,quantity
for unlimited (digital goods/services)null
tags are searchable categories — use multiple for discoverabilityt
is an array ofspecs
pairs for structured display[key, value]- Product
MUST match a zoneshipping[].id
from the parent stallid - Total shipping = stall base cost + (product shipping cost × quantity)
Marketplace UI (Kind:30019) — Addressable
Custom marketplace configuration grouping merchants together:
{ "kind": 30019, "tags": [["d", "my-marketplace"]], "content": "{\"name\":\"Bitcoin Bazaar\",\"about\":\"A curated marketplace for Bitcoin products\",\"ui\":{\"picture\":\"https://example.com/logo.png\",\"banner\":\"https://example.com/banner.jpg\",\"theme\":\"dark\",\"darkMode\":true},\"merchants\":[\"pubkey1hex...\",\"pubkey2hex...\"]}" }
Auction (Kind:30020) — Addressable
Auctions are similar to products but with bidding mechanics:
{ "kind": 30020, "tags": [["d", "auction-rare-item-001"]], "content": "{\"id\":\"auction-rare-item-001\",\"stall_id\":\"my-stall-001\",\"name\":\"Signed Hal Finney Print\",\"description\":\"Limited edition signed print\",\"images\":[\"https://example.com/print.jpg\"],\"starting_bid\":50000,\"start_date\":1719391096,\"duration\":86400,\"specs\":[[\"condition\",\"mint\"],[\"authentication\",\"verified\"]],\"shipping\":[{\"id\":\"worldwide\",\"cost\":20.00}]}" }
Rules:
is in the stall's currency (integer)starting_bid
is a Unix timestamp; omit if start date is unknownstart_date
is seconds the auction runs (excluding extensions)duration- Actual end =
start_date + duration + SUM(all duration_extended from confirmations) - Cannot edit auction after receiving first bid — bids reference the event ID (not product UUID), so editing creates a new event ID and loses all bids
Bid (Kind:1021) — Regular
{ "kind": 1021, "content": "75000", "tags": [["e", "<auction-event-id>"]] }
Rules:
is the bid amount as a string (in the auction's currency)content- The
tag references the event ID of the auction, not the product UUIDe - This is why editing an auction after bids destroys those bids
Bid Confirmation (Kind:1022) — Regular
Sent by the merchant to validate bids:
{ "kind": 1022, "content": "{\"status\":\"accepted\",\"message\":\"Bid received and validated\",\"duration_extended\":300}", "tags": [ ["e", "<bid-event-id>"], ["e", "<auction-event-id>"] ] }
Status values:
accepted, rejected, pending, winner
is sent to the winning bid after auction endswinner
(optional): seconds added to auction durationduration_extended- Clients must verify the confirmation pubkey matches the merchant's pubkey
3. Checkout Flow (NIP-04 DMs)
Checkout uses encrypted direct messages. See references/checkout-flow.md for full details.
Step 1 — Customer sends order (type:0):
{ "id": "order-uuid-123", "type": 0, "name": "Alice", "address": "123 Bitcoin St, Miami, FL", "message": "Please ship ASAP", "contact": { "nostr": "<customer-pubkey-hex>", "phone": null, "email": "alice@example.com" }, "items": [ { "product_id": "product-xyz-789", "quantity": 2 } ], "shipping_id": "us-domestic" }
Step 2 — Merchant sends payment request (type:1):
{ "id": "order-uuid-123", "type": 1, "message": "Payment required within 15 minutes", "payment_options": [ { "type": "ln", "link": "lnbc..." }, { "type": "btc", "link": "bc1q..." }, { "type": "url", "link": "https://pay.example.com/order-123" } ] }
Step 3 — Merchant sends status update (type:2):
{ "id": "order-uuid-123", "type": 2, "message": "Payment confirmed, shipping tomorrow", "paid": true, "shipped": false }
4. P2P Orders (NIP-69 Kind:38383)
For peer-to-peer bitcoin trading. See references/marketplace-events.md for full tag reference.
{ "kind": 38383, "tags": [ ["d", "order-uuid-456"], ["k", "sell"], ["f", "USD"], ["s", "pending"], ["amt", "100000"], ["fa", "50"], ["pm", "zelle", "cashapp"], ["premium", "3"], ["network", "mainnet"], ["layer", "lightning"], ["name", "SatoshiTrader"], ["bond", "1000"], ["expires_at", "1719391096"], ["expiration", "1719995896"], ["y", "my-platform"], ["z", "order"] ], "content": "" }
Required tags:
d, k, f, s, amt, fa, pm, premium, network,
layer, expires_at, expiration, y, z
Status flow:
pending → in-progress → success | canceled | expired
5. Validate Before Publishing
-
tag matches contentd
(for kinds 30017, 30018, 30020)id - Product
references a valid stallstall_id - Product shipping zone IDs match stall shipping zone IDs
-
is integer or null (not string, not undefined)quantity - Auction
is an integerstarting_bid - Bid
tag references auction event ID (not product UUID)e - Bid confirmation has two
tags (bid + auction)e - Checkout messages use correct
values (0, 1, 2)type - P2P order has
tag set toz"order" - P2P order
tag uses ISO 4217 currency codef - P2P order
tag isk
or"buy""sell" - All timestamps are Unix seconds (not milliseconds)
- Content is stringified JSON where required
Shipping Cost Calculation
Total shipping for an order:
base_cost = stall.shipping[chosen_zone].cost per_product = SUM(product.shipping[chosen_zone].cost × quantity) for each item total_shipping = base_cost + per_product
Example: Stall base shipping $5.99, buying 2 units of a product with $10 extra shipping per unit → total shipping = $5.99 + (2 × $10) = $25.99.
Common Mistakes
| Mistake | Why It Breaks | Fix |
|---|---|---|
tag doesn't match content | Relay can't address the event correctly | Always keep tag and content identical |
Sequential IDs (, , ) | Collision risk across merchants | Use UUIDs or descriptive slugs |
| Editing auction after bids received | Bids reference event ID which changes on edit | Never edit auctions that have bids |
| Bid references product UUID instead of event ID | Bid won't be associated with the auction | Use the auction's Nostr event ID in the tag |
Missing tag on P2P orders | Clients can't identify the event as an order | Always include |
Using / without ISO 4217 tag | Currency is ambiguous | Use standard codes: USD, EUR, BRL, VES |
| Product shipping zone ID doesn't match stall | Shipping calculation breaks | Product shipping IDs must exist in parent stall |
as string instead of int | Type mismatch in clients | Use integer or null, never string |
Checkout type as string instead of int | Message routing fails | Use integer type values: 0, 1, 2 |
Bid confirmation missing auction tag | Can't link confirmation to auction | Include both bid and auction tags |
Quick Reference
| Event | Kind | Category | Key Fields |
|---|---|---|---|
| Stall | 30017 | Addressable | id, name, currency, shipping zones |
| Product | 30018 | Addressable | id, stall_id, price, quantity, specs, shipping |
| Marketplace UI | 30019 | Addressable | name, ui config, merchant pubkeys |
| Auction | 30020 | Addressable | id, stall_id, starting_bid, start_date, duration |
| Bid | 1021 | Regular | amount in content, e tag → auction event ID |
| Bid Confirmation | 1022 | Regular | status, e tags → bid + auction |
| P2P Order | 38383 | Addressable | k (buy/sell), f (currency), s (status), amt, fa |
Key Principles
-
IDs must be consistent — The
tag and thed
field inside content JSON must always match. This is how addressable events are located.id -
Shipping is hierarchical — Stalls define base shipping zones and costs. Products add per-unit costs on top. The customer picks one zone at checkout.
-
Auctions are immutable after bids — Because bids reference the event ID (not a UUID), editing an auction creates a new event and orphans existing bids. Design auctions carefully before publishing.
-
Checkout is sequential — Order (type:0) → Payment request (type:1) → Status update (type:2). Each message references the same order ID. All messages are NIP-04 encrypted DMs.
-
P2P orders are self-contained — All trade parameters live in tags, not content. The
tag must bez
for client discovery. Status transitions follow: pending → in-progress → success/canceled/expired."order"