Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
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://relayer.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

Basic example (without signature):
import WebSocket from 'ws';
 
const ws = new WebSocket('wss://relayer.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
    }
  }));
});
With signature (required by some market makers like the vault to respond with actionable bids):
import WebSocket from 'ws';
import { 
  createAuctionStartSiweMessage, 
  extractSiweDomainAndUri 
} from '@sapience/sdk';
import { privateKeyToAccount } from 'viem/accounts';
 
const ws = new WebSocket('wss://relayer.sapience.xyz/auction');
const account = privateKeyToAccount('0xYourPrivateKey...');
 
ws.on('open', async () => {
  const payload = {
    taker: account.address,
    wager: '1000000000000000000',
    resolver: '0xResolverContract...',
    predictedOutcomes: ['0xEncodedPick1', '0xEncodedPick2'],
    takerNonce: 1,
    chainId: 42161
  };
 
  // Generate SIWE signature
  const { domain, uri } = extractSiweDomainAndUri('wss://relayer.sapience.xyz/auction');
  const issuedAt = new Date().toISOString();
  const message = createAuctionStartSiweMessage(payload, domain, uri, issuedAt);
  const signature = await account.signMessage({ message });
 
  ws.send(JSON.stringify({
    type: 'auction.start',
    payload: {
      ...payload,
      takerSignature: signature,
      takerSignedAt: issuedAt
    }
  }));
});
 
// 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;
    // Filter out quote-only bids (zero address/signature) and expired bids
    const actionableBids = bids.filter(b => {
      return b.maker !== '0x0000000000000000000000000000000000000000' &&
             b.makerSignature !== '0x0000000000000000000000000000000000000000000000000000000000000000' &&
             b.makerDeadline > Date.now() / 1000;
    });
    // Select best bid by highest makerWager
    actionableBids.sort((a, b) => BigInt(b.makerWager) > BigInt(a.makerWager) ? 1 : -1);
    console.log('Best actionable bid:', actionableBids[0]);
  }
});

Responder: Submit a Bid

const ws = new WebSocket('wss://relayer.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...) - EOA or smart account
    wager: string,              // Wager amount in wei
    resolver: string,           // Resolver contract address
    predictedOutcomes: string[], // Encoded picks (bytes strings)
    takerNonce: number,
    chainId: number,
    takerSignature?: string,    // Optional: EIP-191 signature of the taker (SIWE format)
    takerSignedAt?: string,     // Optional: ISO timestamp when signature was created (required if takerSignature is provided)
    sessionApproval?: string,   // ZeroDev session approval (base64) for smart account auth
    sessionTypedData?: object   // EIP-712 typed data (required when sessionApproval is provided)
  }
}

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

Signature Requirements:
  • takerSignature and takerSignedAt are optional fields
  • Unsigned requests: Used for price discovery/quoting only. Market makers may respond with quote-only bids (see below)
  • Signed requests: Required by some market makers (like the vault) to respond with actionable, signed bids that can be executed on-chain
  • When provided, the relayer verifies the signature to ensure the requester authorized the auction request
  • Use the SDK's createAuctionStartSiweMessage and extractSiweDomainAndUri helpers to generate the signature
Smart Account Authentication:
  • For smart account users (ZeroDev Kernel accounts), the sessionApproval and sessionTypedData fields enable session-based authentication
  • Both fields are required together - sessionTypedData must be provided when using sessionApproval
  • The sessionApproval is a base64-encoded ZeroDev permission account (with private key stripped for security)
  • The sessionTypedData contains the EIP-712 typed data that was signed during session creation
  • This allows the relayer to verify smart account ownership without on-chain RPC calls
  • The relayer recovers the owner from the enable signature and verifies the smart account address matches

bid.submit

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

Note: The makerSignature is verified on-chain when the taker accepts the bid and calls mint(). The relayer performs structural validation only.

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
    }>
  }
}
Quote-only vs Actionable Bids:
  • Quote-only bids: When maker is 0x0000000000000000000000000000000000000000 and makerSignature is 0x0000... (all zeros), this indicates a price quote only, not an actionable bid that can be executed on-chain. These are typically returned in response to unsigned auction requests.
  • Actionable bids: When maker is a valid address and makerSignature is a real signature, the bid can be accepted and executed on-chain. These are returned in response to signed auction requests (required by some market makers like the vault).

Validation

Auction (auction.start):

  • predictedOutcomes has ≥1 non-empty bytes string
  • taker is a valid address
  • resolver is provided
  • If takerSignature is provided, it must be a valid EIP-191 signature and takerSignedAt must be provided
  • If signature verification fails, the relayer returns error: 'invalid_signature' or error: 'signature_verification_failed'

Bid (bid.submit):

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

Smart Account Authentication

The relayer supports ZeroDev Kernel smart accounts with session-based authentication for auction requesters (takers). This allows requesters to create a session once and submit multiple auction requests without signing each one individually with their wallet.

Note: Bid signatures (makerSignature) are not verified by the relayer. They are verified on-chain when the taker accepts a bid.

Authentication Flow

The relayer uses a 3-path verification strategy:

  1. Session Approval Path (for auction.start only): If sessionApproval and sessionTypedData are provided:

    • Parse the base64-encoded approval to extract the enable signature
    • Use the provided typed data to recover the owner who authorized the session
    • Compute the expected smart account address from the recovered owner
    • Verify it matches the claimed taker address
    • Extract the authorized session key from the signed validatorData
    • Verify the request signature (takerSignature) was signed by that authorized session key
  2. EOA Path: Try direct signature verification against the address (works for EOAs)

  3. Smart Account Owner Path: If EOA verification fails:

    • Recover the signer from the signature
    • Compute their smart account address
    • Verify it matches the claimed address

Security Properties

  • No RPC calls: All verification is deterministic using CREATE2 address computation
  • One-time session signature: Users sign once during session creation; the enable signature proves ownership
  • Counterfactual accounts: Works even for smart accounts that haven't been deployed yet
  • Unspoofable: The smart account address is deterministically derived from the owner's EOA
  • Session key extraction: The authorized session key is extracted from the cryptographically signed validatorData, not from client-provided data. This prevents attackers from claiming authorization for arbitrary session keys

Session Typed Data Structure

The sessionTypedData object has the following structure:

{
  domain: {
    name: 'Kernel',
    version: '0.3.1',
    chainId: number,
    verifyingContract: string  // Smart account address
  },
  types: {
    Enable: [
      { name: 'validationId', type: 'bytes21' },
      { name: 'nonce', type: 'uint32' },
      { name: 'hook', type: 'address' },
      { name: 'validatorData', type: 'bytes' },
      { name: 'hookData', type: 'bytes' },
      { name: 'selectorData', type: 'bytes' }
    ]
  },
  primaryType: 'Enable',
  message: {
    validationId: string,  // Hex
    nonce: number,
    hook: string,         // Address
    validatorData: string, // Hex
    hookData: string,      // Hex
    selectorData: string   // Hex
  }
}

This typed data is captured during session creation and must be included with requests for reliable verification.

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 auction.ack.payload.error or bid.ack.payload.error:

Auction errors:
  • invalid_signature – Taker signature verification failed
  • signature_verification_failed – Error during signature verification process
Session authentication errors:
  • typed_data_requiredsessionTypedData must be provided when using sessionApproval
  • chain_id_mismatch – Chain ID in payload doesn't match typed data domain
  • verifying_contract_mismatch – Typed data verifyingContract doesn't match claimed address
  • account_mismatch – Account address in approval doesn't match claimed address
  • owner_mismatch – Recovered owner's smart account doesn't match claimed address
Bid errors:
  • 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