NOSTR_AUCTION_PROTOCOLS.md raw

Nostr Auction Protocols Specification

Version: 1.0.0 Status: Draft Author: ORLY Development Team

Table of Contents

  1. Overview
  2. Auction Types
  3. Payment Mechanism: Cashu Proof of Funds
  4. Core Protocol Architecture
  5. Event Kinds
  6. Auction Type Implementations
  7. Security Considerations
  8. Implementation Guide

Overview

This specification defines a suite of auction protocols built on Nostr, leveraging the relay network as both a database and real-time message broker. The protocols support multiple auction formats with cryptographic proof of funds using Cashu ecash tokens.

Key Features

Architecture Principles

  1. Regular Events (replaceable): Auction listings, final results (stored in relay DB)
  2. Ephemeral Events: Bid updates, status changes (real-time, not stored)
  3. Encrypted Events: Payment proofs, seller notifications (privacy)
  4. Cashu Proofs: Locked ecash tokens prove bidder capacity

Auction Types

1. English Auction (Ascending Price)

Description: The classic open-outcry auction where bidders compete by placing increasingly higher bids. The highest bidder when the timer expires wins.

Use Cases:

Characteristics:

2. Dutch Auction (Descending Price)

Description: Price starts high and decreases at regular intervals until a buyer accepts the current price or reserve is reached.

Use Cases:

Characteristics:

3. Sealed-Bid First-Price Auction

Description: Bidders submit secret bids, highest bid wins and pays their bid amount.

Use Cases:

Characteristics:

4. Vickrey Auction (Sealed-Bid Second-Price)

Description: Sealed bids where highest bidder wins but pays the second-highest bid amount.

Use Cases:

Characteristics:

5. Reverse Auction

Description: Buyers post requirements, sellers compete by offering decreasing prices.

Use Cases:

Characteristics:

6. Candle Auction

Description: Auction with random, unpredictable end time. Historically, auction ended when a candle flame went out.

Use Cases:

Characteristics:

7. All-Pay Auction

Description: All bidders pay their bid regardless of winning, highest bidder wins the item.

Use Cases:

Characteristics:

Payment Mechanism: Cashu Proof of Funds

Overview

The auction system uses Cashu ecash for trustless proof of funds. Bidders lock tokens with cryptographic proofs without revealing actual funds until payment is required.

Proof of Funds Flow

┌─────────┐         ┌──────────┐         ┌─────────┐         ┌────────┐
│ Bidder  │         │  Mint    │         │ Auction │         │ Seller │
└────┬────┘         └────┬─────┘         └────┬────┘         └───┬────┘
     │                   │                    │                   │
     │ 1. Request Token  │                    │                   │
     ├──────────────────>│                    │                   │
     │                   │                    │                   │
     │ 2. Blinded Token  │                    │                   │
     │<──────────────────┤                    │                   │
     │                   │                    │                   │
     │ 3. Generate Proof │                    │                   │
     │   (DLEQ Proof)    │                    │                   │
     │                   │                    │                   │
     │ 4. Submit Bid + Encrypted Proof        │                   │
     ├───────────────────────────────────────>│                   │
     │                   │                    │                   │
     │                   │ 5. Verify Proof    │                   │
     │                   │<───────────────────┤                   │
     │                   │                    │                   │
     │                   │ 6. Proof Valid     │                   │
     │                   ├───────────────────>│                   │
     │                   │                    │                   │
     │                   │                    │ 7. Bid Accepted   │
     │<───────────────────────────────────────┤                   │
     │                   │                    │                   │
     │                   │                    │ [AUCTION ENDS]    │
     │                   │                    │                   │
     │                   │                    │ 8. Winner Notice  │
     │                   │                    ├──────────────────>│
     │                   │                    │                   │
     │ 9. Request Unlock │                    │                   │
     │<──────────────────────────────────────────────────────────┤
     │                   │                    │                   │
     │ 10. Provide Token │                    │                   │
     ├──────────────────────────────────────────────────────────>│
     │                   │                    │                   │
     │                   │ 11. Redeem Token   │                   │
     │                   │<───────────────────────────────────────┤
     │                   │                    │                   │
     │                   │ 12. Payment        │                   │
     │                   ├───────────────────────────────────────>│

Cashu Token Structure

{
  "token": {
    "mint": "https://mint.example.com",
    "proofs": [
      {
        "id": "00882760bfa2eb41",
        "amount": 1000,
        "secret": "daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9",
        "C": "024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6e3f114d0"
      }
    ]
  }
}

DLEQ (Discrete Log Equality) Proof

Proves knowledge of secret without revealing it:

{
  "dleq_proof": {
    "e": "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9",
    "s": "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9",
    "r": "a6d13fcd7a18442e6076f5e1e7c887ad5de40a019e7f1ae9bae5feb9046acf2f"
  }
}

Proof Verification Steps

  1. Bidder generates blinded token from Cashu mint
  2. Creates DLEQ proof showing control without revealing secret
  3. Encrypts proof using auction server's public key (NIP-04 or NIP-44)
  4. Auction server verifies with mint that proof is valid and amount matches bid
  5. Token remains locked until winner is determined
  6. Winner provides unlock (reveals secret) to complete payment
  7. Seller redeems token from mint

Core Protocol Architecture

Event Types

KindTypePurposeStorage
30020ReplaceableAuction ListingPersistent
30021ReplaceableAuction State UpdatePersistent
30022ReplaceableFinal Auction ResultPersistent
1020RegularBid SubmissionPersistent (sealed)
20020EphemeralBid Update NotificationNot stored
20021EphemeralAuction Status ChangeNot stored
4EncryptedPayment ProofPersistent
14EncryptedSeller NotificationPersistent

Tag Conventions

Event Kinds

Kind 30020: Auction Listing

Purpose: Create new auction listing Replaceable: Yes (by d tag) Storage: Persistent

{
  "kind": 30020,
  "pubkey": "<seller_pubkey>",
  "created_at": 1234567890,
  "tags": [
    ["d", "<auction_id>"],
    ["auction_type", "english"],
    ["title", "Rare Satoshi Nakamoto Signature"],
    ["description", "First known digital signature by Satoshi"],
    ["image", "https://example.com/image.jpg"],
    ["mint", "https://mint.example.com"],
    ["amount", "1000000"],
    ["reserve", "500000"],
    ["currency", "sat"],
    ["start_time", "1234567890"],
    ["end_time", "1234657890"],
    ["increment", "10000"],
    ["extension_time", "300"],
    ["status", "pending"]
  ],
  "content": "Detailed item description and auction terms..."
}

Kind 1020: Bid Submission

Purpose: Submit a bid Replaceable: No Storage: Persistent (for sealed auctions, content encrypted)

{
  "kind": 1020,
  "pubkey": "<bidder_pubkey>",
  "created_at": 1234567890,
  "tags": [
    ["e", "<auction_event_id>"],
    ["d", "<auction_id>"],
    ["auction_type", "english"],
    ["amount", "1100000"],
    ["proof_hash", "<sha256_of_payment_proof>"]
  ],
  "content": ""
}

For sealed-bid auctions, the amount is encrypted:

{
  "kind": 1020,
  "pubkey": "<bidder_pubkey>",
  "created_at": 1234567890,
  "tags": [
    ["e", "<auction_event_id>"],
    ["d", "<auction_id>"],
    ["auction_type", "sealed_first"],
    ["proof_hash", "<sha256_of_payment_proof>"]
  ],
  "content": "<encrypted_bid_data>"
}

Kind 4: Encrypted Payment Proof

Purpose: Send Cashu proof to auction server Encrypted: Yes (NIP-04 or NIP-44) Storage: Persistent

{
  "kind": 4,
  "pubkey": "<bidder_pubkey>",
  "created_at": 1234567890,
  "tags": [
    ["p", "<auction_server_pubkey>"],
    ["e", "<bid_event_id>"],
    ["d", "<auction_id>"]
  ],
  "content": "<encrypted_json>"
}

Decrypted content:

{
  "cashu_token": {
    "mint": "https://mint.example.com",
    "proofs": [...]
  },
  "dleq_proof": {
    "e": "...",
    "s": "...",
    "r": "..."
  },
  "amount": 1100000
}

Kind 20020: Bid Update Notification (Ephemeral)

Purpose: Real-time bid update broadcast Ephemeral: Yes (not stored by relay) Storage: None

{
  "kind": 20020,
  "pubkey": "<auction_server_pubkey>",
  "created_at": 1234567890,
  "tags": [
    ["e", "<auction_event_id>"],
    ["d", "<auction_id>"],
    ["current_bid", "1100000"],
    ["bid_count", "15"],
    ["time_remaining", "3600"],
    ["leading_bidder_hash", "<partial_hash_for_privacy>"]
  ],
  "content": "New bid received"
}

Kind 30021: Auction State Update

Purpose: Update auction status (price changes, extensions) Replaceable: Yes (by d tag) Storage: Persistent

{
  "kind": 30021,
  "pubkey": "<auction_server_pubkey>",
  "created_at": 1234567890,
  "tags": [
    ["d", "<auction_id>"],
    ["status", "active"],
    ["current_price", "1100000"],
    ["bid_count", "15"],
    ["end_time", "1234657890"],
    ["last_bid_time", "1234567800"]
  ],
  "content": ""
}

Kind 30022: Final Auction Result

Purpose: Record final outcome Replaceable: Yes (by d tag) Storage: Persistent

{
  "kind": 30022,
  "pubkey": "<auction_server_pubkey>",
  "created_at": 1234567890,
  "tags": [
    ["d", "<auction_id>"],
    ["status", "ended"],
    ["winner", "<winner_npub>"],
    ["winning_bid", "1100000"],
    ["bid_count", "15"],
    ["settlement_status", "pending"]
  ],
  "content": "Auction ended successfully"
}

Kind 14: Encrypted Seller Notification

Purpose: Notify seller of payment proof Encrypted: Yes Storage: Persistent

{
  "kind": 14,
  "pubkey": "<auction_server_pubkey>",
  "created_at": 1234567890,
  "tags": [
    ["p", "<seller_pubkey>"],
    ["d", "<auction_id>"]
  ],
  "content": "<encrypted_json>"
}

Decrypted content:

{
  "auction_id": "<auction_id>",
  "winner": "<winner_npub>",
  "amount": 1100000,
  "cashu_token_preview": {
    "mint": "https://mint.example.com",
    "amount": 1100000,
    "proof_count": 3
  },
  "unlock_request_method": "Request unlock via DM to winner"
}

Auction Type Implementations

1. English Auction Implementation

Setup Phase

{
  "kind": 30020,
  "tags": [
    ["d", "eng_001"],
    ["auction_type", "english"],
    ["amount", "100000"],
    ["reserve", "80000"],
    ["increment", "5000"],
    ["start_time", "1234567890"],
    ["end_time", "1234657890"],
    ["extension_time", "300"],
    ["auto_extend", "true"]
  ]
}

Bidding Phase

  1. Client subscribes to auction updates:

`json ["REQ", "auction_001", { "kinds": [20020, 30021], "tags": [["d", ["eng_001"]]] }] `

  1. Bidder places bid:

- Creates Kind 1020 bid event - Sends Kind 4 encrypted payment proof - Server validates: bid >= current_price + increment

  1. Server broadcasts Kind 20020 ephemeral update:

`json { "kind": 20020, "tags": [ ["d", "eng_001"], ["current_bid", "105000"], ["bid_count", "3"], ["time_remaining", "7200"] ] } `

  1. Anti-sniping: If bid placed in last 5 minutes, extend by extension_time

Completion Phase

  1. Timer expires or no new bids for extension period
  2. Server publishes Kind 30022 final result
  3. Server sends Kind 14 encrypted notification to seller
  4. Seller requests unlock via DM to winner
  5. Winner provides full Cashu token secret
  6. Seller redeems token from mint

Example Flow

Time    Event
------  --------------------------------------------------
T+0     Seller creates auction (Kind 30020)
T+10    Bidder A bids 105k (Kind 1020 + Kind 4 proof)
T+11    Server broadcasts update (Kind 20020)
T+20    Bidder B bids 110k
T+3595  Bidder C bids 115k (5 min before end)
T+3600  Timer extends to T+3900
T+3900  Auction ends (Kind 30022)
T+3901  Seller gets notification (Kind 14)
T+3920  Seller DMs winner for unlock
T+3925  Winner provides token secret
T+3930  Seller redeems 115k sats

2. Dutch Auction Implementation

Setup Phase

{
  "kind": 30020,
  "tags": [
    ["d", "dutch_001"],
    ["auction_type", "dutch"],
    ["start_price", "500000"],
    ["reserve_price", "100000"],
    ["decrement", "10000"],
    ["interval", "60"],
    ["start_time", "1234567890"],
    ["max_duration", "3600"]
  ]
}

Price Descent

Server publishes Kind 30021 every interval seconds:

{
  "kind": 30021,
  "tags": [
    ["d", "dutch_001"],
    ["current_price", "450000"],
    ["time_elapsed", "300"],
    ["status", "active"]
  ]
}

Price formula: current_price = max(start_price - (decrement * intervals_elapsed), reserve_price)

Acceptance Phase

  1. First buyer to submit valid bid wins at current price
  2. Bid validation:

- Payment proof amount >= current_price - No prior accepted bid exists

  1. Immediate settlement:

`json { "kind": 30022, "tags": [ ["d", "dutch_001"], ["status", "ended"], ["winner", "<buyer_npub>"], ["winning_bid", "450000"], ["final_price", "450000"] ] } `

Example Flow

Time    Price    Event
------  -------  ------------------------------------------
T+0     500000   Auction starts
T+60    490000   Price drops (Kind 30021)
T+120   480000   Price drops
T+180   470000   Price drops
T+240   460000   Price drops
T+300   450000   Buyer accepts! (Kind 1020 + proof)
T+301   450000   Auction ends (Kind 30022)

3. Sealed-Bid First-Price Auction

Setup Phase

{
  "kind": 30020,
  "tags": [
    ["d", "sealed_001"],
    ["auction_type", "sealed_first"],
    ["reserve", "100000"],
    ["start_time", "1234567890"],
    ["end_time", "1234657890"],
    ["reveal_time", "1234668890"]
  ]
}

Sealed Bidding Phase

Bids are encrypted using auction server's public key:

{
  "kind": 1020,
  "tags": [
    ["d", "sealed_001"],
    ["auction_type", "sealed_first"],
    ["proof_hash", "<hash>"]
  ],
  "content": "<encrypted_bid>"
}

Encrypted content structure:

{
  "amount": 250000,
  "bid_secret": "<random_nonce>",
  "timestamp": 1234567900
}

Reveal Phase

After end_time, server decrypts all bids and publishes results:

{
  "kind": 30022,
  "tags": [
    ["d", "sealed_001"],
    ["status", "ended"],
    ["winner", "<winner_npub>"],
    ["winning_bid", "250000"],
    ["second_bid", "230000"],
    ["bid_count", "12"]
  ]
}

Optional: Publish proof of all bids (privacy-preserving):

{
  "kind": 30023,
  "tags": [
    ["d", "sealed_001"],
    ["bid_proof", "<bidder_hash_1>", "250000"],
    ["bid_proof", "<bidder_hash_2>", "230000"],
    ["bid_proof", "<bidder_hash_3>", "210000"]
  ],
  "content": "Bid verification data"
}

Security Considerations

4. Vickrey Auction (Second-Price Sealed-Bid)

Setup Phase

{
  "kind": 30020,
  "tags": [
    ["d", "vickrey_001"],
    ["auction_type", "vickrey"],
    ["reserve", "100000"],
    ["start_time", "1234567890"],
    ["end_time", "1234657890"]
  ]
}

Bidding and Reveal

Identical to sealed-bid first-price, but winner pays second-highest bid:

{
  "kind": 30022,
  "tags": [
    ["d", "vickrey_001"],
    ["winner", "<winner_npub>"],
    ["winning_bid", "250000"],
    ["payment_amount", "230000"],
    ["bid_count", "12"]
  ]
}

Payment Adjustment

Winner proves overpayment and receives refund:

  1. Winner locked 250k in Cashu proof
  2. Winner provides unlock for 230k portion
  3. Winner retains 20k difference

Alternative: Winner provides two separate proofs (230k + 20k) and only unlocks first.

5. Reverse Auction Implementation

Setup Phase (Buyer Posts Requirement)

{
  "kind": 30020,
  "tags": [
    ["d", "reverse_001"],
    ["auction_type", "reverse"],
    ["max_budget", "500000"],
    ["start_time", "1234567890"],
    ["end_time", "1234657890"],
    ["requirements", "<hash_of_detailed_specs>"]
  ],
  "content": "Need custom Nostr client with features X, Y, Z"
}

Seller Bids (Decreasing Prices)

{
  "kind": 1020,
  "tags": [
    ["d", "reverse_001"],
    ["amount", "450000"],
    ["delivery_time", "30"],
    ["portfolio", "https://example.com/portfolio"]
  ],
  "content": "Proposal details..."
}

Winner Selection

Buyer evaluates and selects winning proposal:

{
  "kind": 30022,
  "tags": [
    ["d", "reverse_001"],
    ["winner", "<seller_npub>"],
    ["winning_bid", "400000"],
    ["selection_criteria", "best_value"]
  ]
}

6. Candle Auction Implementation

Setup Phase

{
  "kind": 30020,
  "tags": [
    ["d", "candle_001"],
    ["auction_type", "candle"],
    ["amount", "100000"],
    ["increment", "5000"],
    ["start_time", "1234567890"],
    ["end_window_start", "1234657890"],
    ["end_window_end", "1234667890"],
    ["termination_method", "vrf"]
  ]
}

Random Termination

Server uses Verifiable Random Function (VRF) to determine end time:

{
  "kind": 30022,
  "tags": [
    ["d", "candle_001"],
    ["winner", "<winner_npub>"],
    ["winning_bid", "145000"],
    ["termination_time", "1234662500"],
    ["vrf_proof", "<proof>"],
    ["vrf_seed", "<public_seed>"]
  ]
}

VRF Termination

  1. Public seed announced at auction start
  2. VRF evaluated using server's private key + seed
  3. Random timestamp generated within [start, end] window
  4. Winning bid is highest valid bid before random timestamp
  5. VRF proof allows anyone to verify randomness

Example

Time       Bid     Status
---------  ------  ----------------------------------
1234657890 100k    Window opens
1234659000 105k    Bid A
1234660000 110k    Bid B
1234662000 115k    Bid C
1234662500 ---     [Random termination time via VRF]
1234664000 120k    Bid D (too late!)
1234667890 ---     Window closes

Winner: Bid C (115k) - last valid bid before VRF time

7. All-Pay Auction Implementation

Setup Phase

{
  "kind": 30020,
  "tags": [
    ["d", "allpay_001"],
    ["auction_type", "all_pay"],
    ["prize_value", "1000000"],
    ["min_bid", "10000"],
    ["start_time", "1234567890"],
    ["end_time", "1234657890"]
  ]
}

Bidding Phase

All bidders immediately pay when bidding:

{
  "kind": 1020,
  "tags": [
    ["d", "allpay_001"],
    ["amount", "50000"],
    ["payment_status", "paid"]
  ]
}

Payment proof required before bid acceptance:

{
  "kind": 4,
  "tags": [
    ["p", "<auction_server_pubkey>"],
    ["d", "allpay_001"]
  ],
  "content": "<encrypted_cashu_proof>"
}

Settlement

  1. All bidders unlock Cashu tokens → seller/beneficiary receives all payments
  2. Highest bidder receives the prize
  3. Total collected = sum of all bids
{
  "kind": 30022,
  "tags": [
    ["d", "allpay_001"],
    ["winner", "<winner_npub>"],
    ["winning_bid", "250000"],
    ["total_collected", "1500000"],
    ["bid_count", "25"]
  ]
}

Use Case Example: Charity Auction

Security Considerations

1. Payment Proof Verification

Threat: Fake payment proofs Mitigation:

2. Bid Manipulation

Threat: Sybil attacks, shill bidding Mitigation:

3. Sealed-Bid Privacy

Threat: Bid leakage before reveal Mitigation:

4. Candle Auction Fairness

Threat: Predictable termination time Mitigation:

5. Seller Trust

Threat: Seller doesn't deliver item Mitigation:

6. Network Attacks

Threat: Relay censorship, DoS Mitigation:

7. Time Synchronization

Threat: Clock skew causing unfair bid timing Mitigation:

Implementation Guide

Server Implementation (Relay Plugin)

1. Core Auction Manager

type AuctionManager struct {
    db          database.Database
    publishers  *publish.Publishers
    mintClient  *cashu.MintClient
    activeAuctions map[string]*Auction
}

func (am *AuctionManager) HandleAuctionEvent(event *protocol.Event) error {
    switch event.Kind {
    case 30020: // New auction
        return am.createAuction(event)
    case 1020: // New bid
        return am.processBid(event)
    case 4: // Payment proof
        return am.verifyPaymentProof(event)
    }
}

2. Auction State Machine

type AuctionState int

const (
    StatePending AuctionState = iota
    StateActive
    StateEnded
    StateSettled
    StateCancelled
)

type Auction struct {
    ID           string
    Type         AuctionType
    State        AuctionState
    CurrentPrice uint64
    Bids         []*Bid
    Winner       *Bid
    EndTime      time.Time
}

3. Bid Validation

func (am *AuctionManager) validateBid(auction *Auction, bid *Bid) error {
    // Check auction is active
    if auction.State != StateActive {
        return ErrAuctionNotActive
    }

    // Verify payment proof
    proof, err := am.getPaymentProof(bid.ProofHash)
    if err != nil {
        return err
    }

    // Verify with Cashu mint
    valid, err := am.mintClient.VerifyProof(proof)
    if err != nil || !valid {
        return ErrInvalidPaymentProof
    }

    // Check amount meets requirements
    switch auction.Type {
    case AuctionTypeEnglish:
        minBid := auction.CurrentPrice + auction.Increment
        if bid.Amount < minBid {
            return ErrBidTooLow
        }
    case AuctionTypeDutch:
        if bid.Amount < auction.CurrentPrice {
            return ErrBidTooLow
        }
    }

    return nil
}

4. Real-Time Updates

func (am *AuctionManager) broadcastBidUpdate(auction *Auction, bid *Bid) {
    update := &protocol.Event{
        Kind:      20020, // Ephemeral bid update
        CreatedAt: time.Now().Unix(),
        Tags: [][]string{
            {"d", auction.ID},
            {"current_bid", strconv.FormatUint(bid.Amount, 10)},
            {"bid_count", strconv.Itoa(len(auction.Bids))},
            {"time_remaining", strconv.Itoa(int(time.Until(auction.EndTime).Seconds()))},
        },
    }

    am.publishers.Publish(update)
}

5. Cashu Integration

type CashuClient struct {
    mintURL string
    client  *http.Client
}

func (c *CashuClient) VerifyProof(proof *CashuProof) (bool, error) {
    // Verify DLEQ proof
    if !c.verifyDLEQ(proof) {
        return false, nil
    }

    // Check proof validity with mint
    resp, err := c.client.Post(
        c.mintURL + "/check",
        "application/json",
        encodeProof(proof),
    )

    if err != nil {
        return false, err
    }

    var result CheckProofResponse
    json.NewDecoder(resp.Body).Decode(&result)

    return result.Valid, nil
}

Client Implementation

1. Auction Discovery

// Subscribe to active auctions
const filter = {
  kinds: [30020],
  tags: [["status", "active"]],
  since: Math.floor(Date.now() / 1000) - 86400 // Last 24 hours
};

relay.subscribe([filter], (event) => {
  displayAuction(parseAuction(event));
});

2. Real-Time Bid Updates

// Subscribe to specific auction updates
const auctionId = "eng_001";

const updateFilter = {
  kinds: [20020, 30021], // Ephemeral + state updates
  tags: [["d", auctionId]]
};

relay.subscribe([updateFilter], (event) => {
  if (event.kind === 20020) {
    updateBidDisplay(event);
  } else if (event.kind === 30021) {
    updateAuctionState(event);
  }
});

3. Placing a Bid

async function placeBid(auctionId, amount) {
  // 1. Get Cashu token from mint
  const token = await cashuWallet.requestToken(amount);

  // 2. Generate DLEQ proof
  const proof = await cashuWallet.generateProof(token);

  // 3. Create bid event
  const bidEvent = {
    kind: 1020,
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ["d", auctionId],
      ["amount", amount.toString()],
      ["proof_hash", sha256(JSON.stringify(proof))]
    ],
    content: ""
  };

  const signedBid = await nostr.signEvent(bidEvent);

  // 4. Encrypt and send payment proof
  const encryptedProof = await nostr.nip04.encrypt(
    auctionServerPubkey,
    JSON.stringify({ token, proof, amount })
  );

  const proofEvent = {
    kind: 4,
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ["p", auctionServerPubkey],
      ["e", signedBid.id],
      ["d", auctionId]
    ],
    content: encryptedProof
  };

  const signedProof = await nostr.signEvent(proofEvent);

  // 5. Publish both events
  await relay.publish(signedBid);
  await relay.publish(signedProof);
}

4. Handling Win Notification

// Subscribe to auction results
const resultFilter = {
  kinds: [30022],
  tags: [["d", auctionId]]
};

relay.subscribe([resultFilter], async (event) => {
  const winner = event.tags.find(t => t[0] === "winner")?.[1];

  if (winner === myNpub) {
    // We won! Wait for seller to request unlock
    await handleWinnerFlow(event);
  }
});

async function handleWinnerFlow(resultEvent) {
  // Subscribe to encrypted DMs from seller
  const dmFilter = {
    kinds: [4],
    authors: [sellerPubkey],
    "#p": [myPubkey]
  };

  relay.subscribe([dmFilter], async (dm) => {
    const decrypted = await nostr.nip04.decrypt(sellerPubkey, dm.content);
    const request = JSON.parse(decrypted);

    if (request.type === "unlock_request") {
      // Send the Cashu token secret
      await sendTokenUnlock(request.auction_id);
    }
  });
}

Relay Integration

1. Database Schema

-- Auctions table
CREATE TABLE auctions (
    id TEXT PRIMARY KEY,
    type TEXT NOT NULL,
    seller_pubkey TEXT NOT NULL,
    state TEXT NOT NULL,
    start_time INTEGER NOT NULL,
    end_time INTEGER NOT NULL,
    current_price INTEGER NOT NULL,
    reserve_price INTEGER,
    winner_pubkey TEXT,
    created_at INTEGER NOT NULL
);

-- Bids table
CREATE TABLE bids (
    id TEXT PRIMARY KEY,
    auction_id TEXT NOT NULL,
    bidder_pubkey TEXT NOT NULL,
    amount INTEGER NOT NULL,
    proof_hash TEXT NOT NULL,
    proof_verified BOOLEAN DEFAULT FALSE,
    created_at INTEGER NOT NULL,
    FOREIGN KEY (auction_id) REFERENCES auctions(id)
);

-- Payment proofs table
CREATE TABLE payment_proofs (
    bid_id TEXT PRIMARY KEY,
    cashu_token_hash TEXT NOT NULL,
    mint_url TEXT NOT NULL,
    verified BOOLEAN DEFAULT FALSE,
    unlocked BOOLEAN DEFAULT FALSE,
    FOREIGN KEY (bid_id) REFERENCES bids(id)
);

2. Event Handlers

// In app/handle-auction.go

func (s *Server) handleAuctionMessage(ws *websocket.Conn, msg []byte) error {
    var env protocol.Envelope
    if err := json.Unmarshal(msg, &env); err != nil {
        return err
    }

    switch env.Label {
    case "EVENT":
        return s.handleAuctionEvent(ws, env.Event)
    case "REQ":
        return s.handleAuctionQuery(ws, env.SubscriptionID, env.Filters)
    }

    return nil
}

func (s *Server) handleAuctionEvent(ws *websocket.Conn, event *protocol.Event) error {
    switch event.Kind {
    case 30020: // Auction listing
        return s.auctionManager.CreateAuction(event)
    case 1020: // Bid
        return s.auctionManager.ProcessBid(event)
    case 4: // Payment proof
        return s.auctionManager.VerifyPaymentProof(event)
    }

    return nil
}

Advanced Features

1. Multi-Item Auctions

Support selling multiple identical items:

{
  "kind": 30020,
  "tags": [
    ["d", "multi_001"],
    ["auction_type", "english_multi"],
    ["quantity", "10"],
    ["amount", "50000"],
    ["winners", "10"]
  ]
}

Top 10 bidders each win one item at their bid price.

2. Batch Auctions

Collect bids over time, execute all at once:

{
  "kind": 30020,
  "tags": [
    ["d", "batch_001"],
    ["auction_type", "batch"],
    ["execution_time", "1234567890"],
    ["clearing_method", "uniform_price"]
  ]
}

All winners pay same clearing price.

3. Combinatorial Auctions

Bidders can bid on bundles of items:

{
  "kind": 1020,
  "tags": [
    ["d", "combo_001"],
    ["bundle", "item_a", "item_b", "item_c"],
    ["amount", "500000"]
  ]
}

Requires optimization algorithm to determine winning combinations.

4. Proxy Bidding

Automated bidding on behalf of user up to max price:

{
  "kind": 1021,
  "tags": [
    ["d", "eng_001"],
    ["max_bid", "500000"],
    ["increment", "5000"],
    ["auto_bid", "true"]
  ]
}

Server automatically places bids when outbid, up to max.


Testing & Validation

Test Auction Server

# Start test relay with auction support
export ORLY_AUCTION_ENABLED=true
export ORLY_CASHU_MINT_URL=https://testnut.cashu.space
./orly

Test Client

// Create test auction
const auction = await createAuction({
  type: "english",
  startPrice: 10000,
  reserve: 5000,
  duration: 3600
});

// Simulate bids
await placeBid(auction.id, 11000);
await placeBid(auction.id, 12000);

// Verify winner
const result = await waitForResult(auction.id);
assert(result.winner === expectedWinner);

Compliance Checklist

Future Enhancements

1. Cross-Chain Settlements

Support Bitcoin Lightning, liquid, or other payment rails:

{
  "kind": 4,
  "tags": [
    ["payment_method", "lightning"],
    ["invoice", "lnbc..."]
  ]
}

2. Dispute Resolution

Third-party arbitration system:

{
  "kind": 30024,
  "tags": [
    ["d", "dispute_001"],
    ["auction_id", "eng_001"],
    ["arbitrator", "<npub>"],
    ["status", "pending"]
  ]
}

3. Reputation System

Track seller/buyer reliability:

{
  "kind": 30025,
  "tags": [
    ["d", "<user_pubkey>"],
    ["auctions_completed", "150"],
    ["average_rating", "4.8"],
    ["badges", "trusted_seller", "fast_payer"]
  ]
}

4. Analytics Dashboard

Track auction performance metrics in web UI.

Conclusion

This specification provides a comprehensive framework for implementing decentralized auctions on Nostr. The combination of relay infrastructure, Cashu payment proofs, and flexible event types enables trustless, private, and efficient auction mechanisms for diverse use cases.

Next Steps:

  1. Implement auction manager in ORLY relay
  2. Create reference client library
  3. Deploy test auctions on testnet
  4. Gather community feedback
  5. Submit formal NIP proposal

Contributing: Feedback and contributions welcome at the ORLY repository.

Document Version: 1.0.0 Last Updated: 2025-11-17 License: MIT