18 KiB
NIP-15
Nostr Marketplace
draft
optional
Based on https://github.com/lnbits/Diagon-Alley.
Implemented in NostrMarket and Plebeian Market.
Terms
merchant
- Seller of products with NOSTR key-paircustomer
- Buyer of products with NOSTR key-pairstall
- List of products controlled bymerchant
(amerchant
can have multiple stalls)product
- Item for sale by themerchant
auction
- Item for sale, as auction, by themerchant
bid
- Bids from users participating in a auction.marketplace
- clientside software for searchingstalls
and purchasingproducts
checkout events
- A series of private messages exchanged betweenmerchant
andcustomer
, representing the checkout process.
Note
Nostr Marketplaces operates using Bitcoin as its currency, but
merchants
have the flexibility to set prices in their preferred currency for theirstalls
. This means they can convert from their chosen currency to set a fixed price. Alternatively, merchants can opt to use Bitcoin as their currency, ensuring a fixed price in BTC.
Nostr Marketplace Clients
Merchant admin
The merchant
admin software is where the merchant
creates, updates, and deletes stalls
and products
, as well as manages sales, payments, and communication with customers
. While the merchant
admin software can be purely client-side, implementations will likely have a server client listening for NOSTR events for convenience and uptime. The merchant
admin software can be integrated into the marketplace client.
Marketplace
Marketplace
software should be entirely client-side, either as a standalone 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 similar to any other e-commerce site, with a 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 | |
---|---|---|
30017 |
set_stall |
Create or update a stall. |
30018 |
set_product |
Create or update a product. |
30020 |
set_auction |
Create or update a product selled as auction. |
Alongside other kinds of events that can be involved.
Kind | Description | |
---|---|---|
4 |
direct_message |
Used for checkout events and communication between merchant - customer . |
5 |
delete |
Delete a product or a stall. |
Event 30017
: Create or update a stall.
Event Content
{
"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>]
}
]
}
Observations:
currency
defines the currency in which the merchant wishes to operate. If it'sBTC
, no conversion is required; if it's different fromBTC
, the platform should perform the necessary conversion toBTC
at the time of purchase, ensuring accurate price conversion for each buyer.- Ideally
currency
andregions
values for shipping costs will utilize ISO 3166 codes to ensure accurate and consistent calculations.
Fields that are not self-explanatory:
shipping
:- an array with possible shipping zones for this stall.
- the customer MUST choose exactly one of those shipping zones.
- shipping to different zones can have different costs. For some goods (digital for example) the cost can be zero.
- the
id
is an internal value used by the merchant. This value must be sent back as the customer selection. - each shipping zone contains the base cost for orders made to that shipping zone, but a specific shipping cost per product can also be specified if the shipping cost for that product is higher than what's specified by the base cost.
Event Tags
{
"tags": [["d", <string, id of the stall>]],
...
}
- the
d
tag is required and represents the stall id.
Event 30018
: Create or update a product
Event Content
{
"stall_id": <string, id of the stall to which this product belong to>,
"name": <string, product name>,
"type": <string, "simple" | "variable" | "variation">,
"description": <string (optional), product description>,
"images": <[string], array of image URLs, optional>,
"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>
}
]
}
Observations:
- The product inherits its currency from the stall it belongs to.
Fields that are not self-explanatory:
quantity
: can be null in the case of items with unlimited availability, like digital items, or servicesspecs
:- an optional array of key pair values. It allows for the Customer UI to present product specifications in a structure mode. It also allows comparison between products
- eg:
[["operating_system", "Android 12.0"], ["screen_size", "6.4 inches"], ["connector_type", "USB Type C"]]
shipping
:- an optional array of extra costs to be used per shipping zone, only for products that require special shipping costs to be added to the base shipping cost defined in the stall
- the
id
should match the id of the shipping zone, as defined in theshipping
field of the stall - to calculate the total cost of shipping for an order, the user will choose a shipping option during checkout, and then the client must consider this costs:
- the
base cost from the stall
for the chosen shipping option - the result of multiplying the product units by the
shipping costs specified in the product
, if any.
- the
type
:This determination categorizes a product as either asimple
product, avariable
product, or avariation
of a variable product. For example, a shirt sold in a single color is asimple
product, while a shirt offered in multiple colors is avariable
product withvariations
. To establish a link between a variable product and its variations, the variation product event should include ana
tag that references the event coordinate of the corresponding variable product.
Event Tags
"tags": [
["d", <string, id of product>],
["a", <event coordinate (decoded naddr) of the variable product (optional, only for variations)>, <recommended relay URL, optional>, <string, "variation">],
["t", <string (optional), product category],
["t", <string (optional), product category],
...
],
...
- The
d
tag is required. - The
t
tag is as searchable tag, it represents different categories that the product can be part of (food
,fruits
). Multiplet
tags can be present. - The
a
tag is employed in products that are variations of a variable product, serving as a linking mechanism. It follows the conventions outlined in nip01
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>
}
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:
url
URL to a payment page, stripe, paypal, btcpayserver, etcbtc
onchain bitcoin addressln
bitcoin lightning invoicelnurl
bitcoin lnurl-pay
{
"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,
"status":<string, "pending" | "processing" | "completed" | "cancelled" | "refunded" | "failed">;,
"message": <string, message to customer>,
"paid": <bool: has received payment>,
"shipped": <bool: has been shipped>,
}
Possible order statuses meaning:
Pending
- The order has been created there is something that maintains it on hold, i.e: payment has not been received yet, or the stock of the product has been reduced but the merchant needs to approve the payment.Processing
- The payment has been received and the order is awaiting fulfillment (i.e. shipping products)Completed
- The payment is successful and the order has been fulfilled.Cancelled
- The order has been manually cancelled by the admin or the customer.Refunded
- The order has been refunded by the admin.Failed
- The order has failed due to technical issues or the payment being declined.
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:
{
"stall_id": <String, UUID of the stall to which this product belong to>,
"name": <String, product name>,
"description": <String (optional), product description>,
"type": <string, "simple" | "variable" | "variation">,
"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.
-
The
start_date
can be set to a date in the future if the auction is scheduled to start on that date, or can be omitted if the start date is unknown/hidden. If the start date is not specified, the auction will have to be edited later to set an actual date. -
The auction runs for an initial number of seconds after the
start_date
, specified byduration
.
"tags": [
["d", <string, id of product auction>],
["a", <event coordinate (decoded naddr) of the variable product (optional, only for variations)>, <recommended relay URL, optional>, <string,"variation">],
["t", <string (optional), product category],
["t", <string (optional), product category],
...
],
...
- The
d
tag is required.
Event 1021
: Bid
{
"content": <stringified auction event>,
"tags": [
["a", <event coordinate of the auction to bid on>],
["amount": <int, in the currency of the auction>]
]
}
Bids are simply events of kind 1021
with a content
field containing the strigified version of the auction event, and tags consisting of a tag:a
pointing to the auction event coordinates and a tag:amount
containing the bid amount expressed as an integer in the auction currency. Bids must refer to an auction.
Event 1022
: Bid confirmation
Event Content:
{
"status": <String, "accepted" | "rejected" | "pending" | "winner">,
"message": <String (optional)>,
}
Event Tags:
{
"tags": [
["e" <event ID of the bid being confirmed>],
["a", <event coordinate of the auction>]
],
}
To ensure the validity of bids, merchants must confirm them before they can be considered valid by other clients. Therefore, clients should subscribe to 'bid confirmation' events (kind 1022
) for every auction they follow, in addition to the actual bids. When checking the bid confirmation, clients must verify 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 object that includes, at a minimum, a status
field. The winner
field is used to respond to the winning bid after the auction ends and the merchant has selected the winner.
The reasons for marking a bid as rejected
or pending
are determined by the merchant's implementation and configuration. These reasons can range from basic validation errors (e.g., amount too low) to the bidder being blacklisted or lacking sufficient trust, which may lead to the bid being marked as pending
until additional verification is performed. The key difference between rejected
and pending
bids is that pending
bids may be approved after the bidder takes additional steps, whereas rejected
bids cannot be later approved. Note that pending bids can be re-evaluated after an update to the auction event, at which point the merchant may publish an update to the starting bid field and mark bids with lower values as pending.
An optional message
field can appear in the content
JSON to provide further context for why a bid is rejected
or pending
.
If bids are placed very close to the auction's end date, the merchant can extend the auction duration by updating the duration
field in the auction event.
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/04.md.
Additional
Standard data models can be found here