NIPs nostr improvement proposals

NIP-15 - Nostr Marketplace

Table of Contents

Nostr Marketplace

draft optional

Based on https://github.com/lnbits/Diagon-Alley.

Implemented in NostrMarket and Plebeian Market.

Terms

Nostr Marketplace Clients

Merchant admin

Where the merchant creates, updates and deletes stalls and products, as well as where they manage sales, payments and communication with customers.

The merchant admin software can be purely clientside, but for convenience and uptime, implementations will likely have a server client listening for NOSTR events.

Marketplace

Marketplace software should be entirely clientside, either as a stand-alone app, or as a purely frontend webpage. A customer subscribes to different merchant NOSTR public keys, and those merchants stalls and products become listed and searchable. The marketplace client is like any other ecommerce site, with basket and checkout. Marketplaces may also wish to include a customer support area for direct message communication with merchants.

Merchant publishing/updating products (event)

A merchant can publish these events:

Kind Description
0 set_meta The merchant description (similar with any nostr public key).
30017 set_stall Create or update a stall.
30018 set_product Create or update a product.
4 direct_message Communicate with the customer. The messages can be plain-text or JSON.
5 delete Delete a product or a stall.

Event 30017: Create or update a stall.

Event Content

{
"id": <string, id generated by the merchant. Sequential IDs (`0`, `1`, `2`...) are discouraged>,
"name": <string, stall name>,
"description": <string (optional), stall description>,
"currency": <string, currency used>,
"shipping": [
{
"id": <string, id of the shipping zone, generated by the merchant>,
"name": <string (optional), zone name>,
"cost": <float, base cost for shipping. The currency is defined at the stall level>,
"regions": [<string, regions included in this zone>]
}
]
}

Fields that are not self-explanatory:

Event Tags

{
"tags": [["d", <string, id of stall]],
...
}

Event 30018: Create or update a product

Event Content

{
"id": <string, id generated by the merchant (sequential ids are discouraged)>,
"stall_id": <string, id of the stall to which this product belong to>,
"name": <string, product name>,
"description": <string (optional), product description>,
"images": <[string], array of image URLs, optional>,
"currency": <string, currency used>,
"price": <float, cost of product>,
"quantity": <int or null, available items>,
"specs": [
[<string, spec key>, <string, spec value>]
],
"shipping": [
{
"id": <string, id of the shipping zone (must match one of the zones defined for the stall)>,
"cost": <float, extra cost for shipping. The currency is defined at the stall level>
}
]
}

Fields that are not self-explanatory:

Event Tags

"tags": [
["d", <string, id of product],
["t", <string (optional), product category],
["t", <string (optional), product category],
...
],
...

Checkout events

All checkout events are sent as JSON strings using (NIP-04).

The merchant and the customer can exchange JSON messages that represent different actions. Each JSON message MUST have a type field indicating the what the JSON represents. Possible types:

Message Type Sent By Description
0 Customer New Order
1 Merchant Payment Request
2 Merchant Order Status Update

Step 1: customer order (event)

The below JSON goes in content of NIP-04.

{
"id": <string, id generated by the customer>,
"type": 0,
"name": <string (optional), ???>,
"address": <string (optional), for physical goods an address should be provided>,
"message": "<string (optional), message for merchant>,
"contact": {
"nostr": <32-bytes hex of a pubkey>,
"phone": <string (optional), if the customer wants to be contacted by phone>,
"email": <string (optional), if the customer wants to be contacted by email>
},
"items": [
{
"product_id": <string, id of the product>,
"quantity": <int, how many products the customer is ordering>
}
],
"shipping_id": <string, id of the shipping zone>
}

Open: is contact.nostr required?

Step 2: merchant request payment (event)

Sent back from the merchant for payment. Any payment option is valid that the merchant can check.

The below JSON goes in content of NIP-04.

payment_options/type include:

{
"id": <string, id of the order>,
"type": 1,
"message": <string, message to customer, optional>,
"payment_options": [
{
"type": <string, option type>,
"link": <string, url, btc address, ln invoice, etc>
},
{
"type": <string, option type>,
"link": <string, url, btc address, ln invoice, etc>
},
{
"type": <string, option type>,
"link": <string, url, btc address, ln invoice, etc>
}
]
}

Step 3: merchant verify payment/shipped (event)

Once payment has been received and processed.

The below JSON goes in content of NIP-04.

{
"id": <string, id of the order>,
"type": 2,
"message": <string, message to customer>,
"paid": <bool: has received payment>,
"shipped": <bool: has been shipped>,
}

Customize Marketplace

Create a customized user experience using the naddr from NIP-19. The use of naddr enables easy sharing of marketplace events while incorporating a rich set of metadata. This metadata can include relays, merchant profiles, and more. Subsequently, it allows merchants to be grouped into a market, empowering the market creator to configure the marketplace's user interface and user experience, and share that marketplace. This customization can encompass elements such as market name, description, logo, banner, themes, and even color schemes, offering a tailored and unique marketplace experience.

Event 30019: Create or update marketplace UI/UX

Event Content

{
"name": <string (optional), market name>,
"about": <string (optional), market description>,
"ui": {
"picture": <string (optional), market logo image URL>,
"banner": <string (optional), market logo banner URL>,
"theme": <string (optional), market theme>,
"darkMode": <bool, true/false>
},
"merchants": [array of pubkeys (optional)],
...
}

This event leverages naddr to enable comprehensive customization and sharing of marketplace configurations, fostering a unique and engaging marketplace environment.

Auctions

Event 30020: Create or update a product sold as an auction

Event Content:

{
"id": <String, UUID generated by the merchant. Sequential IDs (`0`, `1`, `2`...) are discouraged>,
"stall_id": <String, UUID of the stall to which this product belong to>,
"name": <String, product name>,
"description": <String (optional), product description>,
"images": <[String], array of image URLs, optional>,
"starting_bid": <int>,
"start_date": <int (optional) UNIX timestamp, date the auction started / will start>,
"duration": <int, number of seconds the auction will run for, excluding eventual time extensions that might happen>,
"specs": [
[<String, spec key>, <String, spec value>]
],
"shipping": [
{
"id": <String, UUID of the shipping zone. Must match one of the zones defined for the stall>,
"cost": <float, extra cost for shipping. The currency is defined at the stall level>
}
]
}

[!NOTE] Items sold as an auction are very similar in structure to fixed-price items, with some important differences worth noting.

Event 1021: Bid

{
"content": <int, amount of sats>,
"tags": [["e", <event ID of the auction to bid on>]],
}

Bids are simply events of kind 1021 with a content field specifying the amount, in the currency of the auction. Bids must reference an auction.

[!NOTE] Auctions can be edited as many times as desired (they are "parameterized replaceable events") by the author - even after the start_date, but they cannot be edited after they have received the first bid! This is enforced by the fact that bids reference the event ID of the auction (rather than the product UUID), which changes with every new version of the auctioned product. So a bid is always attached to one "version". Editing the auction after a bid would result in the new product losing the bid!

Event 1022: Bid confirmation

Event Content:

{
"status": <String, "accepted" | "rejected" | "pending" | "winner">,
"message": <String (optional)>,
"duration_extended": <int (optional), number of seconds>
}

Event Tags:

"tags": [["e" <event ID of the bid being confirmed>], ["e", <event ID of the auction>]],

Bids should be confirmed by the merchant before being considered as valid by other clients. So clients should subscribe to bid confirmation events (kind 1022) for every auction that they follow, in addition to the actual bids and should check that the pubkey of the bid confirmation matches the pubkey of the merchant (in addition to checking the signature).

The content field is a JSON which includes at least a status. winner is how the winning bid is replied to after the auction ends and the winning bid is picked by the merchant.

The reasons for which a bid can be marked as rejected or pending are up to the merchant's implementation and configuration - they could be anything from basic validation errors (amount too low) to the bidder being blacklisted or to the bidder lacking sufficient trust, which could lead to the bid being marked as pending until sufficient verification is performed. The difference between the two is that pending bids might get approved after additional steps are taken by the bidder, whereas rejected bids can not be later approved.

An additional message field can appear in the content JSON to give further context as of why a bid is rejected or pending.

Another thing that can happen is - if bids happen very close to the end date of the auction - for the merchant to decide to extend the auction duration for a few more minutes. This is done by passing a duration_extended field as part of a bid confirmation, which would contain a number of seconds by which the initial duration is extended. So the actual end date of an auction is always start_date + duration + (SUM(c.duration_extended) FOR c in all confirmations.

Customer support events

Customer support is handled over whatever communication method was specified. If communicating via nostr, NIP-04 is used https://github.com/nostr-protocol/nips/blob/master/4.

Additional

Standard data models can be found here