npm package: @opinion-labs/opinion-clob-sdk
Description: TypeScript SDK for Opinion Labs Prediction Market CLOB operations
Author: Opinion Labs
Latest version: 0.6.0

# @opinion-labs/opinion-clob-sdk

TypeScript SDK for [Opinion Labs](https://opinion.trade) Prediction Market. Trade on blockchain-based prediction markets with EIP-712 signed orders, Gnosis Safe multi-sig support, and real-time WebSocket updates.

## Installation

```bash
npm install @opinion-labs/opinion-clob-sdk
```

## Quick Start

```typescript
import { Client, CHAIN_ID_BNB_MAINNET, DEFAULT_API_HOST, OrderSide, OrderType } from '@opinion-labs/opinion-clob-sdk';

const client = new Client({
  host: DEFAULT_API_HOST,
  apiKey: 'your_api_key',
  chainId: CHAIN_ID_BNB_MAINNET,
  rpcUrl: 'https://bsc-dataseed.binance.org',
  privateKey: '0x...',
  multiSigAddress: '0x...',
});

// Get markets
const markets = await client.getMarkets({ limit: 10 });
console.log(markets.result.list);

// Place a limit buy order
await client.placeOrder({
  marketId: 1234,
  tokenId: '0x...',
  side: OrderSide.BUY,
  orderType: OrderType.LIMIT_ORDER,
  price: '0.5',
  makerAmountInQuoteToken: '10',
});
```

## Prerequisites

| Requirement | Description |
|-------------|-------------|
| **API Key** | Obtained from the Opinion Labs team |
| **Private Key** | EOA wallet private key for signing orders |
| **Multi-sig Address** | Your Gnosis Safe wallet address (visible in "My Portfolio") |
| **RPC URL** | BNB Chain RPC endpoint |

> **Note:** The signer (private key) and multi-sig address may be different wallets. The signer is an owner of the Safe that authorizes trades.

## Configuration

```typescript
const client = new Client({
  // Required
  host: 'https://openapi.opinion.trade/openapi',
  apiKey: 'your_api_key',
  chainId: 56,                    // BNB Chain (only supported chain)
  rpcUrl: 'https://bsc-dataseed.binance.org',
  privateKey: '0x...',
  multiSigAddress: '0x...',

  // Optional
  proxyUrl: 'http://127.0.0.1:7890',  // HTTP proxy
  quoteTokensCacheTtl: 3600,           // Cache TTL in seconds (default: 3600)
  marketCacheTtl: 300,                 // Cache TTL in seconds (default: 300)
});
```

## API Reference

### Market Data

```typescript
// List markets with filters
const markets = await client.getMarkets({
  topicType: TopicType.BINARY,            // BINARY, CATEGORICAL, or ALL
  status: TopicStatusFilter.ACTIVATED,     // ACTIVATED, RESOLVED, or ALL
  sortBy: TopicSortType.BY_VOLUME_DESC,   // Sort order
  page: 1,
  limit: 20,
});

// Get market details
const market = await client.getMarket(marketId);

// Get categorical market details
const catMarket = await client.getCategoricalMarket(marketId);

// Get orderbook
const orderbook = await client.getOrderbook(tokenId);

// Get latest price
const price = await client.getLatestPrice(tokenId);

// Get price history
const history = await client.getPriceHistory(tokenId, {
  interval: '1h',   // '1m' | '1h' | '1d' | '1w' | 'max'
  startAt: 1700000000,
  endAt: 1700100000,
});

// Get quote tokens
const tokens = await client.getQuoteTokens();

// Get on-chain fee rates
const fees = await client.getFeeRates(tokenId);
```

### Trading

All order operations are **gas-free** (signed off-chain, submitted via API).

```typescript
// Enable trading (one-time approval, requires gas)
await client.enableTrading();

// Place a limit buy order
await client.placeOrder({
  marketId: 1234,
  tokenId: '0x...',
  side: OrderSide.BUY,
  orderType: OrderType.LIMIT_ORDER,
  price: '0.5',
  makerAmountInQuoteToken: '10',  // 10 USDC
});

// Place a limit sell order
await client.placeOrder({
  marketId: 1234,
  tokenId: '0x...',
  side: OrderSide.SELL,
  orderType: OrderType.LIMIT_ORDER,
  price: '0.6',
  makerAmountInBaseToken: '10',   // 10 outcome tokens
});

// Place a market buy order
await client.placeOrder({
  marketId: 1234,
  tokenId: '0x...',
  side: OrderSide.BUY,
  orderType: OrderType.MARKET_ORDER,
  price: '0',
  makerAmountInQuoteToken: '10',
});

// Cancel an order
await client.cancelOrder(orderId);

// Batch operations
await client.placeOrdersBatch([order1, order2, order3]);
await client.cancelOrdersBatch([id1, id2, id3]);

// Cancel all open orders (with optional filters)
await client.cancelAllOrders({ marketId: 1234, side: OrderSide.BUY });
```

### User Data

```typescript
const positions = await client.getMyPositions({ marketId: 1234 });
const trades = await client.getMyTrades({ page: 1, limit: 20 });
const orders = await client.getMyOrders({ status: '1' });  // 1 = open
const order = await client.getOrderById(orderId);
const balances = await client.getMyBalances();
const auth = await client.getUserAuth();
```

### Token Operations

These operations interact directly with the blockchain and **require gas** (BNB).

```typescript
// Split: convert collateral (USDC) into Yes + No tokens
await client.split(marketId, 10000000n);  // amount in wei

// Merge: convert Yes + No tokens back to collateral
await client.merge(marketId, 10000000n);

// Redeem: claim winnings from a resolved market
await client.redeem(marketId);
```

### WebSocket

Subscribe to real-time market updates.

```typescript
const ws = client.createWebSocketClient({
  heartbeatInterval: 30,
  onOpen: () => console.log('Connected'),
  onClose: () => console.log('Disconnected'),
  onError: (err) => console.error(err),
});

await ws.connect();

// Subscribe to orderbook depth updates
ws.subscribeMarketDepthDiff(marketId, (msg) => {
  console.log(`Depth: ${msg.side} ${msg.price} @ ${msg.size}`);
});

// Subscribe to price updates
ws.subscribeMarketLastPrice(marketId, (msg) => {
  console.log(`Price: ${msg.price}`);
});

// Subscribe to trade updates
ws.subscribeMarketLastTrade(marketId, (msg) => {
  console.log(`Trade: ${msg.price} x ${msg.size}`);
});

// Subscribe to your order status updates
ws.subscribeTradeOrderUpdate((msg) => {
  console.log(`Order ${msg.orderId}: ${msg.status}`);
});

// Subscribe to your new trade records
ws.subscribeTradeRecordNew((msg) => {
  console.log(`New trade: ${msg.tradeId}`);
});

// Disconnect when done
await ws.disconnect();
```

## Builder Mode

For platforms that place orders **on behalf of users** without holding their private keys.

```typescript
import { BuilderClient, UserClient } from '@opinion-labs/opinion-clob-sdk';

const builder = new BuilderClient({
  host: 'https://openapi.opinion.trade/openapi',
  builderApiKey: 'your_builder_api_key',
  chainId: 56,
  rpcUrl: 'https://bsc-dataseed.binance.org',
});

// Create a sub-account for a user
const userInfo = await builder.createUser('0xUserWallet...');
// IMPORTANT: userInfo.apikey is returned only once — store it securely

// Build an order for the user to sign
const orderResult = await builder.buildOrderForSigning({
  marketId: 1234,
  tokenId: '0x...',
  userWalletAddress: '0xUserSafeWallet...',
  side: OrderSide.BUY,
  orderType: OrderType.LIMIT_ORDER,
  amount: 10,
  price: '0.5',
});

// User signs the order (client-side)
const user = new UserClient('0xUserPrivateKey...');
const signature = await user.signTypedData(orderResult.typedData);

// Submit the signed order
await builder.placeOrderForUser({
  ...orderResult,
  signature,
  userWalletAddress: '0xUserSafeWallet...',
});
```

## Error Handling

```typescript
import {
  SdkError,
  InvalidParamError,
  OpenApiError,
  BalanceNotEnough,
  InsufficientGasBalance,
} from '@opinion-labs/opinion-clob-sdk';

try {
  await client.placeOrder(orderData);
} catch (err) {
  if (err instanceof InvalidParamError) {
    // Invalid order parameters
  } else if (err instanceof BalanceNotEnough) {
    // Insufficient token balance
  } else if (err instanceof InsufficientGasBalance) {
    // Not enough BNB for gas
  } else if (err instanceof OpenApiError) {
    // API call failed
  }
}
```

## Response Format

All API methods return:

```typescript
{
  errno: number;    // 0 = success
  errmsg: string;   // Error message (empty on success)
  result: T;        // Response payload
}
```

## Workflow

```
1. enableTrading()     One-time approval (requires gas)
                           |
2. split() (optional)  Convert USDC -> Yes/No tokens (requires gas)
                           |
3. placeOrder()        Place limit/market orders (gas-free)
                           |
4. cancelOrder()       Cancel open orders (gas-free)
                           |
5. merge() / redeem()  Convert back or claim winnings (requires gas)
```

## Builder Demo (Recommended for Partners)

### `examples/builder_demo/` — Scenario-based examples for Builder mode integration

Builder mode lets you build your own trading terminal on top of Opinion's
prediction market, with 2,000 free gasless transactions per day.

```bash
cd examples/builder_demo
cp config.example.json config.json
# Edit config.json with your credentials

npx tsx 01_user_onboarding.ts      # Create user + Safe wallet
npx tsx 02_market_discovery.ts     # Browse markets (read-only)
npx tsx 03_enable_trading.ts       # One-time trading setup
npx tsx 04_place_limit_order.ts    # Place a limit order
npx tsx 05_place_market_order.ts   # Place a market order
npx tsx 06_manage_orders.ts        # Query + cancel orders
npx tsx 07_split_and_merge.ts      # Split/merge outcome tokens
npx tsx 08_redeem_and_withdraw.ts  # Redeem winnings + withdraw
npx tsx 10_full_trading_flow.ts    # End-to-end combined flow
```

See [`examples/builder_demo/README.md`](examples/builder_demo/README.md) for the full guide,
architecture overview, and production integration tips.

## Requirements

- Node.js >= 18
- TypeScript >= 5.3

## License

MIT