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
takerin payloads. - Responder: Submits bids offering to take the opposite side. Referred to as
makerin payloads.
The relayer validates messages, broadcasts auctions to all connected clients, and streams bids to subscribed requesters.
Endpoint
wss://relayer.sapience.xyz/auctionThis 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 ──────────│ │
│ │ │- Requester sends
auction.startwith wager and picks - Relayer responds with
auction.ack(includesauctionIdand optionalidif provided) and broadcastsauction.startedto all clients - Responders send
bid.submitwith their wager offer and signature - Relayer replies
bid.ackand streamsauction.bidsto 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
}
}));
});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.
takerSignatureandtakerSignedAtare 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
createAuctionStartSiweMessageandextractSiweDomainAndUrihelpers to generate the signature
- For smart account users (ZeroDev Kernel accounts), the
sessionApprovalandsessionTypedDatafields enable session-based authentication - Both fields are required together -
sessionTypedDatamust be provided when usingsessionApproval - The
sessionApprovalis a base64-encoded ZeroDev permission account (with private key stripped for security) - The
sessionTypedDatacontains 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 bids: When
makeris0x0000000000000000000000000000000000000000andmakerSignatureis0x0000...(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
makeris a valid address andmakerSignatureis 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):
predictedOutcomeshas ≥1 non-empty bytes stringtakeris a valid addressresolveris provided- If
takerSignatureis provided, it must be a valid EIP-191 signature andtakerSignedAtmust be provided - If signature verification fails, the relayer returns
error: 'invalid_signature'orerror: 'signature_verification_failed'
Bid (bid.submit):
auctionIdmatches an active auctionmakerWager> 0makerDeadlineis in the futuremakerSignatureis 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:
-
Session Approval Path (for
auction.startonly): IfsessionApprovalandsessionTypedDataare 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
takeraddress - Extract the authorized session key from the signed
validatorData - Verify the request signature (
takerSignature) was signed by that authorized session key
-
EOA Path: Try direct signature verification against the address (works for EOAs)
-
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:
invalid_signature– Taker signature verification failedsignature_verification_failed– Error during signature verification process
typed_data_required–sessionTypedDatamust be provided when usingsessionApprovalchain_id_mismatch– Chain ID in payload doesn't match typed data domainverifying_contract_mismatch– Typed data verifyingContract doesn't match claimed addressaccount_mismatch– Account address in approval doesn't match claimed addressowner_mismatch– Recovered owner's smart account doesn't match claimed address
invalid_payload– Malformed message structureauction_not_found_or_expired– Unknown or expired auctionquote_expired–makerDeadlinehas passedinvalid_maker_wager– Wager is zero or invalidinvalid_maker– Maker address is invalidinvalid_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