draft
optional
author:fiatjaf
author:benarc
author:motorina0
author:talvasconcelos
Implemented in NostrMarket and Plebeian Market
merchant
- seller of products with NOSTR key-paircustomer
- buyer of products with NOSTR key-pairproduct
- item for sale by themerchant
stall
- list of products controlled bymerchant
(amerchant
can have multiple stalls)marketplace
- clientside software for searchingstalls
and purchasingproducts
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
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
.
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 Content:
{
"id": <String, UUID 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, UUID 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:
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 stall]]
- the
d
tag is required, its value MUST be the same as the stallid
.
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>,
"currency": <String, currency used>,
"price": <float, cost of product>,
"quantity": <int, available items>,
"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>,
}
]
}
Fields that are not self-explanatory:
-
specs
:- 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"]]
Open: better to move
spec
in thetags
section of the event? -
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
Event Tags:
"tags": [
["d", <String, id of product],
["t", <String (optional), product category],
["t", <String (optional), product category],
...
]
- the
d
tag is required, its value MUST be the same as the productid
. - 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.
All checkout events are sent as JSON strings using (NIP04).
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 |
The below json goes in content of NIP04.
{
"id": <String, UUID 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, UUID of the product>,
"quantity": <int, how many products the customer is ordering>
}
],
"shipping_id": <String, UUID of the shipping zone>
}
Open: is contact.nostr
required?
Sent back from the merchant for payment. Any payment option is valid that the merchant can check.
The below json goes in content
of NIP04.
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, UUID 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>
}
]
}
Once payment has been received and processed.
The below json goes in content
of NIP04.
{
"id": <String, UUID of the order>,
"type": 2,
"message": <String, message to customer>,
"paid": <Bool, true/false has received payment>,
"shipped": <Bool, true/false has been shipped>,
}
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.
Standard data models can be found here