Open Markets Protocol
Offers
The public offer model: NIP-99 listings, Gamma commerce metadata, product variations, collections, shipping, identity, and legacy interoperability.
Scope
An offer is a public, signed statement that a person or organization is willing to provide a product, service, rental, subscription, or other item under described terms. Open Markets uses NIP-99 as the canonical offer primitive and adds the commerce fields developed by the Gamma Markets specification.
An offer is not an order, invoice, payment, reservation, or proof of stock. Publishing an offer does not guarantee that the merchant will accept a later order.
Event kinds
| Kind | Purpose | Requirement |
|---|---|---|
30402 | Active public offer | Required |
30403 | Public draft or inactive offer | Optional |
30405 | Product collection | Optional |
30406 | Shipping option | Optional for physical goods |
Kinds 30402, 30403, 30405, and 30406 are addressable events. Their stable address is <kind>:<author-pubkey>:<d-tag>. Clients MUST apply the NIP-01 replacement rule: select the event with the greatest created_at, then the event with the lowest ID when timestamps are equal.
Offer event
A conforming active offer MUST be kind 30402. Kind 30403 has the same offer structure but indicates that the listing is not active. Kind 30403 is still public; private work-in-progress drafts SHOULD instead use NIP-37.
Required tags
| Tag | Cardinality | Meaning |
|---|---|---|
d | exactly 1 | Stable identifier chosen by the author |
title | exactly 1 | Human-readable offer title |
price | exactly 1 | amount, currency, and optional recurrence |
The canonical price shape is:
["price", "18.00", "USD"]
Recurring offers MAY add a frequency:
["price", "15", "EUR", "month"]
Implementations SHOULD emit the NIP-99 noun forms hour, day, week, month, or year. For Gamma compatibility they SHOULD also accept H, D, W, and Y. A bare unit letter is not a complete ISO 8601 duration and MUST NOT be presented as one.
Standard metadata
| Tag | Cardinality | Meaning |
|---|---|---|
summary | 0 or 1 | Short display description |
published_at | 0 or 1 | First publication time in Unix seconds |
status | 0 or 1 | NIP-99 lifecycle: active or sold |
location | 0 or 1 | Human-readable location |
g | 0 or 1 | Geohash |
image | 0 or more | URL, optional dimensions, optional sort order |
t | 0 or more | Category or discovery term |
The event content SHOULD contain the complete human-readable description in Markdown. Structured values needed for filtering or validation belong in tags, not only in content.
Commerce metadata
| Tag | Shape | Meaning |
|---|---|---|
type | type, format | Product relationship and delivery format |
visibility | value | hidden, on-sale, or pre-order |
stock | integer | Available quantity claimed by the merchant |
spec | key, value | Repeatable product property |
weight | value, unit | Physical weight using ISO 80000-1 units |
dim | LxWxH, unit | Physical dimensions |
a | event address | Parent offer or collection reference |
shipping_option | event address, optional extra cost | Available delivery option or collection |
supersedes | offer address | Prior product identity replaced by this offer |
client | NIP-89 tuple | Optional software attribution |
Valid type values are simple, variable, and variation. Valid formats are digital and physical. If omitted, clients MUST interpret the offer as simple and digital. If visibility is omitted, clients MUST interpret it as on-sale.
status and visibility are separate compatibility fields. Clients MUST NOT silently rewrite one from the other. status describes the NIP-99 listing lifecycle; visibility describes Gamma commerce presentation and availability.
Complete example
{
"id": "<event-id>",
"pubkey": "<merchant-pubkey>",
"created_at": 1786924800,
"kind": 30402,
"tags": [
["d", "coffee-ethiopia-250g"],
["title", "Ethiopian Coffee, 250 g"],
["summary", "Whole-bean, light-roast coffee"],
["published_at", "1786924800"],
["price", "18.00", "USD"],
["status", "active"],
["type", "simple", "physical"],
["visibility", "on-sale"],
["stock", "24"],
["image", "https://example.com/coffee.jpg", "1200x1200", "0"],
["spec", "roast", "light"],
["weight", "250", "g"],
["shipping_option", "30406:<merchant-pubkey>:us-standard"],
["t", "coffee"],
["client", "Example Market", "31990:<app-pubkey>:market", "wss://relay.example.com"]
],
"content": "Floral whole-bean coffee from Ethiopia.",
"sig": "<signature>"
}
Validation
A client accepting an offer MUST:
- Validate its NIP-01 event ID and signature.
- Require exactly one non-empty
d, one non-emptytitle, and oneprice. - Reject repeated singleton tags instead of choosing an arbitrary value.
- Parse prices as non-negative decimal strings. Exponent notation, signs, separators,
NaN, and infinity are invalid. - Require fiat currencies to use uppercase ISO 4217 codes. Bitcoin-like or application currencies MAY use established ISO-like codes.
- Require
stockand image sort order to be non-negative base-10 integers when present. - Validate every event address before dereferencing it.
- Ignore unknown tags so extensions remain forward compatible.
- Treat price and stock as claims. The merchant MUST recalculate availability and the payable total when an order arrives.
Clients SHOULD enforce reasonable limits on Markdown length, image count, tag count, and URL size. Clients MUST treat remote media as untrusted content.
Variable products
A variable product describes a product family. Each purchasable variation is its own offer.
- The parent MUST use
type=variable. - A child MUST use
type=variation. - A child MUST include exactly one
atag pointing to its kind30402parent. - The child MUST define its own required title and price.
- Order items MUST point to a purchasable child, not an abstract parent, unless the parent is itself orderable.
["type", "variation", "physical"]
["a", "30402:<merchant-pubkey>:shirt"]
["spec", "size", "M"]
["spec", "color", "violet"]
Clients MUST NOT assume that values cascade from the parent. A variation explicitly carries every value required to price and fulfill it.
Collections
Kind 30405 organizes offers and may provide reusable references. Collection support is optional; an implementation can conform while rendering and purchasing standalone offers.
Required tags:
["d", "summer-roasts"]
["title", "Summer Roasts"]
["a", "30402:<merchant-pubkey>:coffee-ethiopia-250g"]
Optional tags include image, summary, location, g, and repeatable shipping_option references.
Collections do not automatically modify products. An offer MUST explicitly reference a collection resource when it intends to use it. Clients MUST merge direct shipping references with explicitly referenced collection shipping options and MUST NOT infer inheritance merely because a collection lists the offer.
Shipping options
Kind 30406 describes one delivery or pickup method. It MAY be authored by the merchant or a third-party carrier.
Required tags:
| Tag | Meaning |
|---|---|
d | Stable shipping identifier |
title | Display name |
price | Base cost and currency |
country | One or more ISO 3166-1 alpha-2 codes |
service | standard, express, overnight, or pickup |
Optional tags include carrier, region, duration, location, g, minimum and maximum weight or dimensions, and variable price components.
{
"kind": 30406,
"created_at": 1786924800,
"content": "Standard regional shipping",
"tags": [
["d", "us-standard"],
["title", "Standard shipping"],
["price", "5.99", "USD"],
["country", "US"],
["region", "US-FL", "US-GA"],
["service", "standard"],
["carrier", "Example Post"],
["duration", "2", "5", "D"],
["weight-max", "30", "kg"],
["price-weight", "0.75", "USD", "kg"]
],
"pubkey": "<shipping-author-pubkey>",
"id": "<event-id>",
"sig": "<signature>"
}
Open Markets resolves a Gamma inconsistency by defining variable pricing as [price, currency, unit], matching Gamma's example. Clients SHOULD accept the older ambiguous [price, unit] form only when the currency can be determined safely from the base price.
Referencing a third-party shipping event is not an endorsement or guarantee. The merchant remains responsible for presenting the final shipping choice and amount in the order flow.
Product identity
Addressable offers can be updated in place. Operational changes such as price, stock, shipping, and presentation MAY reuse the same d. When the underlying product or service materially changes, the merchant SHOULD publish a new d so reviews and references do not silently move to a different subject.
The new offer MAY identify its predecessor:
["supersedes", "30402:<merchant-pubkey>:old-product-id"]
Rules:
supersedesMUST appear at most once.- It MUST contain a syntactically valid kind
30402address. - It MUST NOT point to the event's own address.
- Clients traversing a chain MUST detect cycles.
- The tag is advisory. It does not delete the predecessor, transfer ownership, authorize a different author, or move reviews.
- Cross-author supersession MUST be shown as an unverified claim unless separately authorized.
This incorporates Gamma PR #8 without making historical ratings appear to describe a materially different product.
Client attribution
Publishing software MAY include the NIP-89 tuple:
["client", "Example Market", "31990:<app-pubkey>:market", "wss://relay.example.com"]
Clients SHOULD include attribution when the user has enabled it and MUST provide an opt-out. Missing or malformed attribution MUST NOT invalidate an offer. Attribution does not prove merchant approval, product authenticity, payment validity, or authorization. This incorporates the non-authoritative attribution model proposed by Gamma PR #11.
NIP-15 compatibility
NIP-15 is marked unrecommended and is not the Open Markets offer format. Its stalls (30017), products (30018), NIP-04 checkout messages, and embedded shipping structures are legacy inputs.
An implementation MAY import a NIP-15 product, but it MUST publish a newly signed kind 30402 event before presenting it as an Open Markets offer. A bridge MUST NOT claim lossless round-trip conversion. Legacy NIP-15 checkout JSON MUST NOT be treated as a kind 16 order without explicit conversion and fresh authorization.
NIP-69 compatibility
NIP-69 kind 38383 describes public P2P Bitcoin/fiat liquidity orders. Despite its name, it is closer to a public exchange offer than a private product checkout order.
Applications MAY expose both protocols, but they MUST keep them distinct. Open Markets does not reuse NIP-69 premiums, bonds, ratings, statuses, expiration rules, or escrow assumptions. Kind 38383 MUST NOT be used for a product purchase order.