Skip to main content

NIP-69: Peer-to-Peer Order Events

NIP-69 defines a standard format for peer-to-peer Bitcoin trading orders, enabling interoperability between P2P platforms.

Summary

AspectDetail
Kind38383
PurposeP2P trade order publication
StatusDraft (optional)
Depends onNIP-01, NIP-33

Overview

NIP-69 enables:

  • Interoperable orders across P2P platforms
  • Aggregated order books from multiple sources
  • Decentralized price discovery for fiat/BTC
  • Platform-agnostic alerts and notifications

Order Event (kind 38383)

Structure

{
"kind": 38383,
"pubkey": "maker_pubkey",
"created_at": 1704067200,
"content": "",
"tags": [
["d", "unique-order-id"],
["k", "sell"],
["f", "EUR"],
["s", "pending"],
["amt", "100000-500000"],
["fa", "100-500"],
["pm", "revolut", "sepa", "wise"],
["premium", "-2.5"],
["source", "https://mostro.network"],
["network", "lightning"],
["layer", "lightning"],
["name", "MostroP2P"],
["expiration", "1704153600"],
["bond", "2"]
]
}

Tag Reference

Required Tags

TagNameDescriptionValues
dIDUnique order identifierString
kKindOrder typebuy, sell
fFiatFiat currency codeISO 4217 (EUR, USD, etc.)
sStatusOrder statusSee status values

Amount Tags

TagNameDescriptionFormat
amtSats AmountBitcoin amount in satsmin-max or exact
faFiat AmountFiat amountmin-max or exact

Examples:

  • ["amt", "100000-500000"] - 100k to 500k sats
  • ["amt", "250000"] - Exactly 250k sats
  • ["fa", "100-500"] - 100 to 500 EUR
  • ["fa", "0"] - Any amount (market order)

Pricing Tags

TagNameDescriptionFormat
premiumPremiumPrice premium/discount %Decimal string

Examples:

  • ["premium", "5"] - 5% above market
  • ["premium", "-2.5"] - 2.5% below market (discount)
  • ["premium", "0"] - Market price

Payment Tags

TagNameDescriptionValues
pmPayment MethodsAccepted payment methodsMultiple values
networkNetworkSettlement networklightning, onchain
layerLayer(Alias for network)lightning, onchain

Common Payment Methods:

revolut, sepa, wise, paypal, venmo, zelle, 
cashapp, strike, n26, pix, bizum, mbway

Metadata Tags

TagNameDescription
sourceSourcePlatform URL
nameNamePlatform name
bondBondRequired bond percentage
expirationExpirationUnix timestamp
yPlatform Typemostro, robosats, etc.

Status Values

StatusDescription
pendingOrder created, awaiting activation
activeOrder is live and can be taken
successTrade completed successfully
canceledOrder canceled by maker
disputeTrade is in dispute
expiredOrder expired

Event Flow

Implementation Examples

Publishing an Order

const orderEvent = {
kind: 38383,
pubkey: makerPubkey,
created_at: Math.floor(Date.now() / 1000),
content: "", // Can be encrypted trade details
tags: [
["d", crypto.randomUUID()],
["k", "sell"],
["f", "EUR"],
["s", "active"],
["amt", "100000-500000"],
["fa", "100-500"],
["pm", "revolut", "sepa"],
["premium", "-2"],
["network", "lightning"],
["source", "https://myplatform.com"],
["expiration", String(Math.floor(Date.now() / 1000) + 86400)]
]
};

orderEvent.id = getEventHash(orderEvent);
orderEvent.sig = signEvent(orderEvent, privateKey);

await pool.publish(relays, orderEvent);

Subscribing to Orders

// Subscribe to EUR sell orders
const filter = {
kinds: [38383],
"#f": ["EUR"],
"#k": ["sell"],
"#s": ["active"],
since: Math.floor(Date.now() / 1000) - 86400
};

const sub = pool.subscribeMany(relays, [filter], {
onevent(event) {
const order = parseOrder(event);
console.log(`${order.kind} ${order.amount} sats @ ${order.premium}%`);
}
});

Parsing Order Events

function parseOrder(event) {
const getTag = (name) => event.tags.find(t => t[0] === name)?.[1];
const getTags = (name) => event.tags.find(t => t[0] === name)?.slice(1) || [];

return {
id: getTag('d'),
kind: getTag('k'), // 'buy' or 'sell'
fiat: getTag('f'), // 'EUR', 'USD', etc.
status: getTag('s'),
amount: parseRange(getTag('amt')),
fiatAmount: parseRange(getTag('fa')),
premium: parseFloat(getTag('premium') || '0'),
paymentMethods: getTags('pm'),
network: getTag('network') || getTag('layer'),
source: getTag('source'),
expiration: parseInt(getTag('expiration') || '0'),
pubkey: event.pubkey
};
}

function parseRange(str) {
if (!str) return null;
const parts = str.split('-');
return parts.length === 2
? { min: parseInt(parts[0]), max: parseInt(parts[1]) }
: { exact: parseInt(parts[0]) };
}

Filtering by Premium

// Find orders with discount (negative premium)
function findDiscounts(orders) {
return orders.filter(o => o.premium < 0)
.sort((a, b) => a.premium - b.premium); // Best discounts first
}

// Find orders within premium range
function findInRange(orders, maxPremium) {
return orders.filter(o => o.premium <= maxPremium);
}

Relay Considerations

Platform-Specific Relays

Each P2P platform operates its own relay:

const p2pRelays = [
'wss://relay.mostro.network', // Mostro
'wss://relay.lnp2pbot.com', // lnp2pBot
'wss://nostr.robosats.org', // RoboSats
'wss://relay.peachbitcoin.com' // Peach Bitcoin
];

Aggregators like p2psats connect to all four to build a unified order book.

Event Retention

  • Orders should include expiration tag
  • Relays may garbage-collect expired orders
  • Clients should filter by status and expiration

Content Encryption

The content field can contain encrypted trade details:

// Platform-specific encrypted content
{
"content": "<NIP-04 encrypted JSON>",
// ... tags
}

// Decrypted content might contain:
{
"contact": "telegram:@trader123",
"terms": "Payment within 15 minutes",
"instructions": "Include order ID in payment reference"
}

Platform Integration

Mostro

  • Native Nostr P2P exchange
  • Uses NIP-59 (GiftWrap) for private messaging
  • ["y", "mostro"] tag identifies source
  • Relay: wss://relay.mostro.network

RoboSats

  • Tor-based P2P exchange
  • Maintains robosats-nostr-sync scraper
  • Also aggregates HodlHodl and Peach orders
  • Relay: wss://nostr.robosats.org

lnp2pBot

  • Telegram bot for P2P trading (@lnp2pBot)
  • Mirrors orders to Nostr
  • Relay: wss://relay.lnp2pbot.com

Peach Bitcoin

  • Mobile P2P app
  • Publishes orders to Nostr
  • Relay: wss://relay.peachbitcoin.com

Security Considerations

Order Authenticity

  • Verify event signature matches pubkey
  • Check that source platform is legitimate
  • Don't trust content without platform verification

Escrow

NIP-69 only defines order publication. Actual trades use platform escrow:

Dispute Resolution

Handled by the originating platform, not the Nostr layer.

See Also


Open Standard

NIP-69 creates network effects: the more platforms publish orders, the more valuable aggregators become, attracting more traders and platforms. It's a virtuous cycle toward unified P2P liquidity.