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.

install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
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"
manifest: data/skills-md/accolver/skill-maker/nostr-marketplace-builder/SKILL.md
source content

Nostr 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

IntentKind(s)NIP
Create/update a merchant stall30017NIP-15
List a product for sale30018NIP-15
Configure marketplace UI30019NIP-15
Create an auction listing30020NIP-15
Place a bid on an auction1021NIP-15
Confirm/reject a bid1022NIP-15
Checkout (order/pay/status)NIP-04 DMsNIP-15
P2P buy/sell order38383NIP-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:

  • d
    tag value MUST equal the
    id
    in content JSON
  • id
    should be a UUID or descriptive slug — sequential IDs (
    0
    ,
    1
    ,
    2
    ) are discouraged
  • currency
    is a string (e.g.,
    "USD"
    ,
    "BTC"
    ,
    "EUR"
    )
  • shipping
    array defines zones; customer must choose exactly one zone at checkout
  • Each zone has a base
    cost
    in the stall's currency

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:

  • d
    tag value MUST equal the
    id
    in content JSON
  • stall_id
    MUST reference an existing stall's
    id
  • quantity
    : integer for limited stock,
    null
    for unlimited (digital goods/services)
  • t
    tags are searchable categories — use multiple for discoverability
  • specs
    is an array of
    [key, value]
    pairs for structured display
  • Product
    shipping[].id
    MUST match a zone
    id
    from the parent stall
  • 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:

  • starting_bid
    is in the stall's currency (integer)
  • start_date
    is a Unix timestamp; omit if start date is unknown
  • duration
    is seconds the auction runs (excluding extensions)
  • 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:

  • content
    is the bid amount as a string (in the auction's currency)
  • The
    e
    tag references the event ID of the auction, not the product UUID
  • 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

  • winner
    is sent to the winning bid after auction ends
  • duration_extended
    (optional): seconds added to auction duration
  • 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

  • d
    tag matches content
    id
    (for kinds 30017, 30018, 30020)
  • Product
    stall_id
    references a valid stall
  • Product shipping zone IDs match stall shipping zone IDs
  • quantity
    is integer or null (not string, not undefined)
  • Auction
    starting_bid
    is an integer
  • Bid
    e
    tag references auction event ID (not product UUID)
  • Bid confirmation has two
    e
    tags (bid + auction)
  • Checkout messages use correct
    type
    values (0, 1, 2)
  • P2P order has
    z
    tag set to
    "order"
  • P2P order
    f
    tag uses ISO 4217 currency code
  • P2P order
    k
    tag is
    "buy"
    or
    "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

MistakeWhy It BreaksFix
d
tag doesn't match content
id
Relay can't address the event correctlyAlways keep
d
tag and content
id
identical
Sequential IDs (
0
,
1
,
2
)
Collision risk across merchantsUse UUIDs or descriptive slugs
Editing auction after bids receivedBids reference event ID which changes on editNever edit auctions that have bids
Bid references product UUID instead of event IDBid won't be associated with the auctionUse the auction's Nostr event ID in the
e
tag
Missing
z
tag on P2P orders
Clients can't identify the event as an orderAlways include
["z", "order"]
Using
buy
/
sell
without ISO 4217
f
tag
Currency is ambiguousUse standard codes: USD, EUR, BRL, VES
Product shipping zone ID doesn't match stallShipping calculation breaksProduct shipping IDs must exist in parent stall
quantity
as string
"10"
instead of int
10
Type mismatch in clientsUse integer or null, never string
Checkout type as string
"0"
instead of int
0
Message routing failsUse integer type values: 0, 1, 2
Bid confirmation missing auction
e
tag
Can't link confirmation to auctionInclude both bid and auction
e
tags

Quick Reference

EventKindCategoryKey Fields
Stall30017Addressableid, name, currency, shipping zones
Product30018Addressableid, stall_id, price, quantity, specs, shipping
Marketplace UI30019Addressablename, ui config, merchant pubkeys
Auction30020Addressableid, stall_id, starting_bid, start_date, duration
Bid1021Regularamount in content, e tag → auction event ID
Bid Confirmation1022Regularstatus, e tags → bid + auction
P2P Order38383Addressablek (buy/sell), f (currency), s (status), amt, fa

Key Principles

  1. IDs must be consistent — The

    d
    tag and the
    id
    field inside content JSON must always match. This is how addressable events are located.

  2. Shipping is hierarchical — Stalls define base shipping zones and costs. Products add per-unit costs on top. The customer picks one zone at checkout.

  3. 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.

  4. 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.

  5. P2P orders are self-contained — All trade parameters live in tags, not content. The

    z
    tag must be
    "order"
    for client discovery. Status transitions follow: pending → in-progress → success/canceled/expired.