Introduction

The objective of this Nostr Specification is to enable two or more parties to transact material information, quote, and close orders in a standardized, structured way with their trading partners. Open Materials Availability (OMA) is an extension of the Nostr Protocol, leveraging Nostr’s federated data exchange model for companies to connect, and transact information in a trustless environment. This specification extends Nostr’s functionality with several key models embedded in Nostr events, enabling supply chain interactions. These models are:
  • Listings – Material supply or demand
  • Quote – A bid or an ask on a published material listing
  • Quote Response – A closure on a quote (accepted, or rejected)
Event sharing is premised on two types of nodes: Network Nodes, and Inbox Nodes. Network nodess are where you publish listing events for trading partners to discover, query and quote. These act as permissioned data repositories for material information. An Inbox Relay is a designated relay for parties to communicate quoting event threads, like email inboxes, only you have access to all available information in this node, but anyone can communicate with you via it.

Events

OMA events are the atomic units of information exchange in the network. Events are defined by Nostr NIP-01. Models used in the network are JSON object embedded into these Nostr events.
{
  "id": "<32-bytes lowercase hex-encoded sha256 of the serialized event data>",
  "pubkey": "<32-bytes lowercase hex-encoded public key of the event creator>",
  "created_at": <unix timestamp in seconds>,
  "kind": <integer between 0 and 65535>,
  "tags": [
    ["<arbitrary string>", "..."]
    // ...
  ],
  "content": "<embedded model>",
  "sig": "<64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data, which is the same as the \"id\" field>"
}

Tags

Along with the content, events are tagged according to a convention. Following Nostr’s convention of indexing single-letter tags only, these are tags and their associated fields the spec uses for events. All tags SHOULD be lowercase, with no spaces. All tags except UserPublicKey are expected to have one value. Format: ["<single-letter>", "<value>"]
  • Address: "a"
  • Identifier: "d"
  • MaterialNo: "n"
  • Manufacturer: "m"
  • Thread: "h"
  • Type: "y"
  • Company: "c"
  • Batch: "b"
  • InternalNo: "i"
  • Generic: "t"
  • Reply Node: "r"
  • Recipient: "p"
  • Status: "s"
  • Finalized: "z"
  • Event: "e"
  • UserPublicKey: "u"

Tag Explanations

Tags serve as queryable and additional identifieng information about an event.

a — Address

  • Use: Referential address of an event for replacement, i.e. when a listing must be updated, it is done so on the address.
  • Example: ["a","<kind integer>:<32-bytes lowercase hex of a pubkey>:<d tag value>"]

d — Identifier

  • Use: stable identifier for a given material, this is a concatination of material information that makes it physically unique among other components with the same part number.
  • Example: ["d","MaterialNo + Supply + DateCode"]

n — MaterialNo

  • Use: material/part number from your system.
  • Example: ["n","MAT-00042"]

m — Manufacturer

  • Use: manufacturer name.
  • Example: ["m","Acme Corp"]

h — Thread

  • Use: groups related events.
  • Value: a deterministic thread ID computed from the root event_id and two participant pubkeys.

y — Type

  • Use: material transaction type (e.g., supply, demand).
  • Example: ["y","supply"]

c — Company

  • Use: event publisher namespace.
  • Example: ["c","acme"]

b — Batch

  • Use: groups listings created by a single publish action (e.g., one API call or one file import).
  • Example: ["b","XYZ123"]

i — InternalNo

  • Use: internal ERP/DB tracking/reference.
  • Example: ["i","INT-7788"]

t — Generic

  • Use: free-form topic tag (search/filter).
  • Example: ["t","diodes"]

r — Reply Node

  • Use: websocket URL for reply events.
  • Example: ["r","wss://domain.seminode.com/inbox"]

p — Recipient

  • Use: intended recipient’s Nostr pubkey.
  • Example: ["p","<pubkey>"]
  • Note: This tag is exclusive to thread events of kind Quote, or Quote Response

s — Status

  • Use: workflow state this event belongs to (e.g., accepted, rejected).
  • Example: ["s","accepted"]
  • Note: This tag is exclusive to thread events of kind Quote Response

z — Finalized

  • Use: workflow state this event belongs to (e.g., pending, complete).
  • Example: ["z","pending"]
  • Note: This tag is exclusive to thread events of kind Quote, or Quote Response

e — Event

  • Use: reference to another event (mention/link).
  • Example: ["e","<event-id>"]

u — UserPublicKey

  • Use: user pubkey(s) associated with the event, allows fine grain filtering for company users.
  • Example(s): ["u","<hex>"] (repeat tag if multiple)

Query Modifying Tags

Query Modifying tags offer further specification for what events the client wants returned. These tags are not stored in events, rather they are passed in filters and modify the underlying query.
  • Match: "match" <Optional["match", "fuzzy"]>
    • Match tag currently supports "fuzzy", for prefix or trigram matching on the above select tags.
  • Intent: "intent" <Optional["intent", "tail"]>
    • The intent tag currently supports the "tail" option.
    • "tail" marks the latest event in a conversation between two parties for a given event.
      • This is determined by the most recent timestamp for a unique combination of:
        • The "e" tag (event)
        • And an unordered pair of the event Author and Recipient tag "p".

Listing

Listings convey specific information about a material. Kind: 31001 Content:
{
  "material_no": "<string - Material Number (Required)>",
  "mfr": "<string - Manufacturer (Required)>",
  "qty": <int - Quantity (Required)>,
  "timing": {
    "lead_time": <string - Lead Time (Optional), supply listings>,
    "required_date": <int64 - Required Date as Unix timestamp (Optional), demand listings>
    "expirey": <int64 - Listing expirey as Unix timestamp (Optional)>
  },
  "unit_price": <float64 - Unit Price (Required)>,
  "internal_no": "<string - Internal Number (Optional)>",
  "type": "<string - 'supply' or 'demand' (Required)>",
  "company": "<string - Publish Company Name (Optional)>",
  "contact": {
    "methods": ["<string - Contact method>"],
    "reply_url": "<string - Contact reply URL>"
  },
  "image_url": "<string – Image URL (Optional)>",
  "attributes": {
    "coo": "<string - Country of Origin (Optional)>",
    "eccn": "<string - Export Control Classification Number (Optional)>",
    "moq": <int - Minimum Order Quantity (Optional)>,
    "mpq": <int - Minimum Pack Quantity (Optional)>,
    "currency": "<string - ISO 4217 Currency Code (Optional)>",
    "lot_no": "<string - Lot Number (Optional)>",
    "custom_attributes": {
      "<string>": "<any - User-defined attributes>"
    },
  },
  "encrypted": {
    "payload": {
      "ciphertext": "<string - Encrypted JSON object (type: object, Optional)>"
    }
  },
  "schema_version": "<string - listing schema version"
}
Listing Tags
[
  ["n", "material_no"],
  ["m", "manufacturer name"],
  ["y", "'supply' or 'demand'"],
  ["c", "company_name"],
  ["b", "any"],
  ["i", "internal_no"],
  ["c", "any"],
  ["t", "any"],
  ["r", "inbox_relay_url"]
]

Quote

Similar to a Listing, a quote is a JSON object embedded in an event content. Quotes’ content, however, are strings of NIP-44 encrypted JSON objects, where the conversation key is derived from your private key and the recipient’s public key. When publishing a quote in reference to a listing, CLIENTS MUST republish the Listing to the relay URL in the listing “r” tag. This preserves the threads integrity as Network Events are mutable. Kind: 2014 Content:
{
  "company": "<string - Company name (set by app layer)>",
  "contact": "<string - Contact information>",
  "message": "<string - Quote message or note>",
  "qty": "<int - Quantity being quoted>",
  "unit_price": "<float64 - Unit price for the quoted item>",
  "timing": {
    "lead_time": <string - Lead Time (Optional), supply listings>,
    "required_date": <int64 - Required Date as Unix timestamp (Optional), demand listings>
    "expirey": <int64 - Listing expirey as Unix timestamp (Optional)>
  },
  "terms": {
    "shipping": {
      "incoterm": "<string - Incoterm (e.g. FOB, CIF)>",
      "place": "<string - Place of delivery or shipment>"
    },
    "payment_term": {
      "code": "<string - Payment term code (e.g. NET30)>",
      "currency": "<string - Currency code (e.g. USD)>"
    }
  },
  "meta": "<object - Arbitrary metadata JSON object (type: object, Optional)>",
  "schema_version": "<string - quote schema version"
}
This object is encrypted, and placed in the content of the event, whereupon retrieval it is decrypted. Quote Tags
[
  ["h", "thread_id"],
  ["n", "material_no"],
  ["m", "manufacturer name"],
  ["y", "supply or demand"],
  ["c", "poster_name"],
  ["b", "any"],
  ["t", "any"],
  ["i", "internal_no"],
  ["c", "any"],

  ["r", "inbox_relay_url"],
  ["p", "recipient_pubkey"],
  ["e", "event_id", "", "root"],
  ["u", "user_pubkey1", "user_pubkey2"]
]
  • Quotes SHOULD copy the listing tags over.
  • Quotes MUST generate a thread_id "h" if not present SEE Thread ID Tag
  • Quotes MUST set the recipient "p" tag to the listing event’s Author pubkey.
  • Quotes MUST be published to the listing’s "r" tag inbox relay URL, as well as the sender’s inbox relay.

Response

Kind: 2024 A response is an event that actions a quote. Responses must either have status "accepted" or "rejected". Like quotes, responses are NIP-44 encrypted between the author’s private key and the recipient’s public key. In OMA, a quoting thread can be closed in two ways: either through a rejection, or through mutual acceptance by both parties. Tagging is used to represent the state of the response. The following tags MUST be set for responses depending on the state.
  • Rejected:
    • ["s", "rejected"]
    • ["z", "complete"]
Quotes that are rejected set the “s” status tag to “rejected”, and mark the quoting threads “z” tag as “complete”
  • Accepted (First Party Acceptance):
    • ["s", "accepted"]
    • ["z", "pending"]
Quotes that are accepted by one party set the “s” status tag to “accepted”, and mark the quoting threads “z” tag as “pending”
  • Accepted (Second Party Acceptance / Final Closure):
    • ["s", "accepted"]
    • ["z", "complete"]
Quotes that are accepted by both parties set the “s” status tag to “accepted”, and mark the quoting threads “z” tag as “complete” Second acceptances may only occur on responses on signed events where the other party has also “accepted” the quote. Content: This is the unencrypted JSON content of a quote response. This payload is NIP-44 encrypted and the resulting string is placed in the content of the event.
{
  "company": "<string - Company name (set by app layer)>",
  "contact": "<string - Contact information>",
  "status": "<string - 'accepted' or 'rejected'>",
  "message": "<string - Quote message or note>",
  "meta": "<object - Arbitrary metadata JSON object (Optional)>",
  "quote": "<object - Embedded quote details (type: Quote)>",
  "reason": "<string - Reserved for future use>",
  "schema_version": "<string - listing schema version"
}
Response Tags
[
  // Inherited Tags from replied-to quote
  ["n", "material_no"],
  ["m", "manufacturer name"],
  ["y", "supply or demand"],
  ["c", "poster_name"],
  ["b", "any"],
  ["i", "internal_no"],
  ["c", "any"],
  ["t", "any"],

  // Reply Event tags
  ["t", "thread_id"],
  ["r", "wss://domain.seminode.com/inbox"],        // My reply inbox relay URL
  ["p", "recipient_pubkey"],       // The recipient pubkey
  ["e", "root_listing_event_id", "", "root"],   // The listing event ID
  ["u", "user_pubkey1", "user_pubkey2"], // Optional, the user pubkey

  // Response Only Tags
  ["s", "accepted", "rejected"],
  ["z", "complete", "pending"]
]
  • Responses SHOULD copy the listing tags over.
  • Responses MUST set the recipient "p" tag to the quote event’s Author pubkey.
  • Responses MUST set the status "s" tag to ‘accepted’ or ‘rejected’.
  • Responses MUST set the status "z" tag to ‘pending’ if they are the first to accept, or ‘compplete’ if they reject, or ar accepting another accepted response.
  • Responses MUST be published to the listing’s "r" tag inbox relay URL, as well as the sender’s inbox relay.

Threads

Threads are chains of quote and response events tied to an underlying listing. This is done via computed “h” tags. The purpose fo the thread id is to carry a unique identifier for an underlying listing, and exchange between two parties through the quoting process. The thread id tag is defined below. Stable definition Given event_id, pubkey1, pubkey2 (all 64-hex, lowercase), where event_id is the underlying root event ID, pubkey1 and pubkey2 are the conversation pubkeys:
  1. Validate event_id is non-empty (and 64-hex if enforcing format).
  2. Canonicalize order: pk_lo = min(pubkey1, pubkey2); pk_hi = max(pubkey1, pubkey2) (lexicographic).
  3. Concatenate (no delimiter): raw = event_id + pk_lo + pk_hi.
  4. Hash: sha256(raw); encode hex lowercasethread_id.