Skip to content

Auction Relayer API

The Auction Relayer is a WebSocket service that facilitates real-time matching between requesters and responders for prediction market positions.

  • Requester: Creates an auction by specifying a wager and predictions (including one or many picks). Referred to as taker in payloads.
  • Responder: Submits bids offering to take the opposite side. Referred to as maker in payloads.

The relayer validates messages, broadcasts auctions to all connected clients, and streams bids to subscribed requesters.

Endpoint

wss://api.sapience.xyz/auction

This is the standard relayer deployment. You can configure a different relayer in the app at sapience.xyz/settings.

Message Flow

Requester                    Relayer                     Responder
    │                           │                            │
    │─── auction.start ────────▶│                            │
    │◀── auction.ack ───────────│                            │
    │                           │─── auction.started ───────▶│
    │                           │                            │
    │                           │◀── bid.submit ─────────────│
    │                           │─── bid.ack ───────────────▶│
    │◀── auction.bids ──────────│                            │
    │                           │                            │
  1. Requester sends auction.start with wager and picks
  2. Relayer responds with auction.ack (includes auctionId and optional id if provided) and broadcasts auction.started to all clients
  3. Responders send bid.submit with their wager offer and signature
  4. Relayer replies bid.ack and streams auction.bids to the requester

Requesters are auto-subscribed to their auction's bid stream.

Quick Start

Requester: Start an Auction

import WebSocket from 'ws';
 
const ws = new WebSocket('wss://api.sapience.xyz/auction');
 
// Initialize an auction
ws.on('open', () => {
  ws.send(JSON.stringify({
    type: 'auction.start',
    payload: {
      taker: '0xRequesterAddress...',
      wager: '1000000000000000000', // 1 USDe in wei
      resolver: '0xResolverContract...',
      predictedOutcomes: ['0xEncodedPick1', '0xEncodedPick2'],
      takerNonce: 1,
      chainId: 42161
    }
  }));
});
 
// Listen for bids
ws.on('message', (data) => {
  const msg = JSON.parse(String(data));
  
  if (msg.type === 'auction.ack') {
    console.log('Auction started:', msg.payload.auctionId);
  }
  
  if (msg.type === 'auction.bids') {
    const bids = msg.payload.bids;
    // Select best bid by highest makerWager among non-expired
    const validBids = bids.filter(b => b.makerDeadline > Date.now() / 1000);
    validBids.sort((a, b) => BigInt(b.makerWager) > BigInt(a.makerWager) ? 1 : -1);
    console.log('Best bid:', validBids[0]);
  }
});

Responder: Submit a Bid

const ws = new WebSocket('wss://api.sapience.xyz/auction');
 
// Listen for bids
ws.onmessage = (ev) => {
  const msg = JSON.parse(String(ev.data));
  
  if (msg.type === 'auction.started') {
    const auction = msg.payload;
    const now = Math.floor(Date.now() / 1000);
    
    ws.send(JSON.stringify({
      type: 'bid.submit',
      payload: {
        auctionId: auction.auctionId,
        maker: '0xResponderAddress...',
        makerWager: (BigInt(auction.wager) / 2n).toString(),
        makerDeadline: now + 60,
        makerSignature: '0x...', // EIP-712 typed signature
        makerNonce: 1
      }
    }));
  }
};

Payloads

auction.start

{
  type: 'auction.start',
  id?: string,                   // Optional request ID for correlation
  payload: {
    taker: string,              // Requester's address (0x...)
    wager: string,              // Wager amount in wei
    resolver: string,           // Resolver contract address
    predictedOutcomes: string[], // Encoded picks (bytes strings)
    takerNonce: number,
    chainId: number
  }
}

If id is provided, it will be echoed back in the auction.ack response for client-side correlation.

bid.submit

{
  type: 'bid.submit',
  payload: {
    auctionId: string,
    maker: string,              // Responder's address
    makerWager: string,         // Responder's wager in wei
    makerDeadline: number,      // Unix timestamp (must be future)
    makerSignature: string,     // EIP-712 signature
    makerNonce: number
  }
}

auction.bids

Streamed to auction subscribers:

{
  type: 'auction.bids',
  payload: {
    auctionId: string,
    bids: Array<{
      auctionId: string,
      maker: string,
      makerWager: string,
      makerDeadline: number,
      makerSignature: string,
      makerNonce: number
    }>
  }
}

Validation

Auction (auction.start):

  • predictedOutcomes has ≥1 non-empty bytes string
  • taker is a valid address
  • resolver is provided

Bid (bid.submit):

  • auctionId matches an active auction
  • makerWager > 0
  • makerDeadline is in the future
  • makerSignature is valid hex format

Additional Messages

auction.subscribe

Subscribe to an existing auction's bid stream without starting a new auction:

{
  type: 'auction.subscribe',
  payload: {
    auctionId: string
  }
}

This is useful when a secondary client needs to monitor bids for an auction started by another connection.

Connection Limits

  • Rate limit: 100 messages per 10 seconds. Exceeding closes connection with code 1008.
  • Message size: Max 64KB per message. Exceeding closes connection with code 1009.

Error Codes

Returned in bid.ack.payload.error:

  • invalid_payload – Malformed message structure
  • auction_not_found_or_expired – Unknown or expired auction
  • quote_expiredmakerDeadline has passed
  • invalid_maker_wager – Wager is zero or invalid
  • invalid_maker – Maker address is invalid
  • invalid_maker_bid_signature_format – Signature format invalid

Bid Acceptance

After selecting a bid, the requester constructs a MintParlayRequestData struct and calls mint() on the PredictionMarket contract. Both parties must have ERC-20 approvals set for the contract to pull their collateral (USDe).

Reference implementation: packages/api/src/auction/botExample.ts