nostr-marketplace-builder
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
- The task involves Nostr marketplace primitives such as stalls, products, auctions, bids, checkout messaging, or NIP-69 orders.
- The user is building commerce flows on Nostr rather than generic social posting.
- The request needs event structures or workflow guidance for merchant listings, shipping, bidding, or order state.
- The problem is marketplace-specific behavior under NIP-15 or NIP-69.
Do NOT use when:
- The task is general Nostr event building with no marketplace semantics.
- The work is relay protocol, client architecture, or filter design.
- The request is generic Lightning or Cashu integration outside a marketplace flow.
Response format
Always structure the final response with these top-level sections, in this order:
- Summary — state the task, scope, and main conclusion in 1-3 sentences.
- Decision / Approach — state the key classification, assumptions, or chosen path.
- Artifacts — provide the primary deliverable(s) for this skill. Use clear subheadings for multiple files, commands, JSON payloads, queries, or documents.
- Validation — state checks performed, important risks, caveats, or unresolved questions.
- Next steps — list concrete follow-up actions, or write
Noneif nothing remains.
Rules:
- Do not omit a section; write
Nonewhen a section does not apply. - If files are produced, list each file path under Artifacts before its contents.
- If commands, JSON, SQL, YAML, or code are produced, put each artifact in fenced code blocks with the correct language tag when possible.
- Keep section names exactly as written above so output stays predictable across skills.
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:
dtag value MUST equal theidin content JSONidshould be a UUID or descriptive slug — sequential IDs (0,1,2) are discouragedcurrencyis a string (e.g.,"USD","BTC","EUR")shippingarray defines zones; customer must choose exactly one zone at checkout- Each zone has a base
costin 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:
dtag value MUST equal theidin content JSONstall_idMUST reference an existing stall'sidquantity: integer for limited stock,nullfor unlimited (digital goods/services)ttags are searchable categories — use multiple for discoverabilityspecsis an array of[key, value]pairs for structured display- Product
shipping[].idMUST match a zoneidfrom 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_bidis in the stall's currency (integer)start_dateis a Unix timestamp; omit if start date is unknowndurationis 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:
contentis the bid amount as a string (in the auction's currency)- The
etag 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
winneris sent to the winning bid after auction endsduration_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
-
dtag matches contentid(for kinds 30017, 30018, 30020) - Product
stall_idreferences a valid stall - Product shipping zone IDs match stall shipping zone IDs
-
quantityis integer or null (not string, not undefined) - Auction
starting_bidis an integer - Bid
etag references auction event ID (not product UUID) - Bid confirmation has two
etags (bid + auction) - Checkout messages use correct
typevalues (0, 1, 2) - P2P order has
ztag set to"order" - P2P order
ftag uses ISO 4217 currency code - P2P order
ktag 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
| Mistake | Why It Breaks | Fix |
|---|---|---|
d tag doesn't match content id |
Relay can't address the event correctly | Always keep d tag and content id identical |
Sequential IDs (0, 1, 2) |
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 e tag |
Missing z tag on P2P orders |
Clients can't identify the event as an order | Always include ["z", "order"] |
Using buy/sell without ISO 4217 f 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 |
quantity as string "10" instead of int 10 |
Type mismatch in clients | Use integer or null, never string |
Checkout type as string "0" instead of int 0 |
Message routing fails | Use integer type values: 0, 1, 2 |
Bid confirmation missing auction e tag |
Can't link confirmation to auction | Include both bid and auction e 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
dtag and theidfield inside content JSON must always match. This is how addressable events are located. -
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
ztag must be"order"for client discovery. Status transitions follow: pending → in-progress → success/canceled/expired.
More from accolver/skill-maker
skill-maker
Create or iteratively improve agent skills with eval-driven refinement when the task is to build a new SKILL.md package or tune an existing skill’s trigger accuracy and performance.
26git-conventional-commits
Generate conventional commit messages from staged git changes when the task is to name or format a commit, not to review code or write release notes.
16pdf-toolkit
Operate the bundled PDF scripts to extract, OCR, create, merge, split, or convert PDFs when the task explicitly involves PDF document processing.
14nostr-event-builder
Construct valid Nostr event JSON and tag structures from requirements when the task is to create or validate concrete events rather than choose which NIPs to use.
12error-handling
Standardize application error handling—taxonomy, propagation, response shapes, logging, and correlation IDs—when the task is to improve consistency of existing error behavior across a codebase.
12pr-description
Generate pull-request descriptions from branch diffs when the task is to explain change scope, motivation, testing, risk, and reviewer guidance for a reviewable branch.
12