rain-sdk-v2
v2.5.8
Published
Rain SDK V2 — TypeScript SDK for Rain prediction markets on Arbitrum. Market creation, trading, liquidity, order book, split/merge, dispute, and smart account support.
Readme
Rain SDK V2
TypeScript SDK for the Rain prediction markets protocol on Arbitrum One. Provides transaction builders for market creation, trading, liquidity management, order book operations, dispute resolution, and smart account (Account Abstraction) support.
⚠️ Security notice — upgrade to >= 2.5.0
Versions below 2.5.0 build sell transactions with no slippage protection by
default: buildSellOptionTx defaulted minAmountOut to 0 and the documented
slippageTolerance option was ignored, exposing sells to MEV/sandwich loss.
Fixed in 2.5.0 — sells now quote getSellProceeds on-chain and apply
slippageTolerance (default 5%). Versions <2.5.0 are deprecated on npm.
Migration is one line: buildSellOptionTx is now async — add await.
// before (< 2.5.0)
const tx = rain.buildSellOptionTx({ /* ... */ });
// after (>= 2.5.0)
const tx = await rain.buildSellOptionTx({ /* ... */ });Installation
npm install rain-sdk-v2 viemOptional (for Smart Account / Account Abstraction)
npm install @alchemy/aa-core @account-kit/infra @account-kit/wallet-clientQuick Start
import { Rain, RainAA, TradingModel, OptionSide } from 'rain-sdk-v2';
import { createWalletClient, custom, parseUnits, parseEther } from 'viem';
import { arbitrum } from 'viem/chains';
// Initialize SDK
const rain = new Rain({
environment: 'development', // 'development' | 'stage' | 'production'
rpcUrl: 'https://arb1.arbitrum.io/rpc', // optional; falls back to a public RPC (pass your own in production)
});
// Get environment config
const config = rain.getEnvironmentConfig();
console.log(config.market_factory_address); // Factory contract
console.log(config.tokens.usdt.address); // USDT token address
console.log(config.tokens.rain.address); // RAIN token address
console.log(config.tokens.usdr.address); // USDR token address
console.log(config.tokens.usdt.decimals); // 6
console.log(config.tokens.rain.decimals); // 18
console.log(config.tokens.usdr.decimals); // 18
// Get token config by address
const tokenInfo = rain.getTokenConfig('0x...');
console.log(tokenInfo?.decimals, tokenInfo?.symbol);
// USDR token ABI (ERC20 + AccessControl + EIP-2612 permit, role-gated mint/burn)
import { USDRAbi } from 'rain-sdk-v2';Classes
Rain
Stateless class for building transactions and querying on-chain data. Does not require a wallet.
Constructor
const rain = new Rain(config?: RainCoreConfig);| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| environment | 'development' \| 'stage' \| 'production' | 'development' | Target environment |
| rpcUrl | string | Fixed public RPC | Custom Arbitrum RPC URL. Omitting it uses a single deterministic public RPC; in production this also emits a console.warn — pass a dedicated endpoint. |
| apiUrl | string | From environment | Custom API URL |
RainAA
Stateful class for smart account (Account Abstraction) management with Alchemy gas sponsorship.
⚠️ Browser-only.
RainAApersists session keys inindexedDB, so it is not supported in Node.js or SSR — session methods throw a descriptive error in those environments. For server-side flows use the statelessRainclass (transaction building and on-chain reads), which runs anywhere.
Constructor
const rainAA = new RainAA(config: RainConfig);| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| walletClient | WalletClient | Yes | Viem wallet client |
| alchemyApiKey | string | Yes | Alchemy API key |
| paymasterPolicyId | string | Yes | Alchemy paymaster policy ID |
| chain | Chain | Yes | Viem chain (e.g. arbitrum) |
| rpcUrl | string | No | Custom RPC URL |
Methods
connect()
Initializes the smart account. Returns the smart account address.
const smartAccountAddress = await rainAA.connect();sendTransaction(rawTx: RawTransaction)
Sends a transaction from the smart account (gas-sponsored).
const hash = await rainAA.sendTransaction(rawTx);disconnect()
Resets the connection state.
rainAA.disconnect();address (getter)
Returns the smart account address. Throws if not connected.
client (getter)
Returns the smart wallet client instance. Throws if not connected.
Transaction Builders
All transaction builders return RawTransaction (or RawTransaction[] for multi-step operations):
interface RawTransaction {
to: `0x${string}`;
data: `0x${string}`;
value?: bigint;
}Market Creation
buildCreateMarketTx(params: CreateMarketTxParams): Promise<RawTransaction[]>
Creates a new prediction market. Returns approval TX (if needed) + createPool TX.
const config = rain.getEnvironmentConfig();
// Create market with USDT (6 decimals)
const txs = await rain.buildCreateMarketTx({
marketQuestion: 'Will ETH reach $5000 by end of 2025?',
marketOptions: ['Yes', 'No'],
marketTags: ['crypto'],
marketDescription: 'Prediction on ETH price target',
isPublic: true,
isPublicPoolResolverAi: true, // true = AI resolver, false = manual resolver
creator: '0x...', // smart account or EOA address
startTime: BigInt(Math.floor(Date.now() / 1000) + 120), // 2 min from now
endTime: BigInt(Math.floor(Date.now() / 1000) + 86400), // 24h from now
no_of_options: 2n,
disputeTimer: 259200, // oracle end time duration in seconds (e.g. 259200 = 3 days)
inputAmountWei: parseUnits('10', 6), // 10 USDT
barValues: [50, 50], // probability distribution (0-100, sums to 100)
initialYesPrices: [500000000000000000n, 500000000000000000n], // optional, 1e18 scale (50% = 5e17)
baseToken: config.tokens.usdt.address, // USDT
tradingModel: TradingModel.AMM, // AMM = 0, OrderBook = 1
marketImage: 'https://cdn.example.com/market-image.png',
});
// Create market with RAIN token (18 decimals)
const txsRain = await rain.buildCreateMarketTx({
...params,
inputAmountWei: parseUnits('10', 18), // 10 RAIN
baseToken: config.tokens.rain.address, // RAIN token
});
// Approval for USDT: liquidity + (oracleFixedFeePerOption * numberOfOptions)
// Approval for RAIN: liquidity + (numberOfOptions * 20% of liquidity)
// e.g. 1000 RAIN with 5 options → 1000 + (5 * 200) = 2000 RAIN approval| Parameter | Type | Description |
|-----------|------|-------------|
| marketQuestion | string | The market question |
| marketOptions | string[] | Option labels. 1 = binary (single Yes/No) market; 2+ = multi-outcome. Length must equal no_of_options |
| marketTags | string[] | Tags (1-3) |
| marketDescription | string | Description |
| isPublic | boolean | Public market |
| isPublicPoolResolverAi | boolean | true for AI resolver, false for manual |
| creator | 0x${string} | Creator/pool owner address |
| startTime | bigint | Unix timestamp (seconds) |
| endTime | bigint | Unix timestamp (seconds) |
| no_of_options | bigint | Number of options |
| inputAmountWei | bigint | Initial liquidity in base token wei |
| disputeTimer | number | Oracle end time duration in seconds (e.g. 259200 = 3 days). Auto-set from environment config |
| barValues | number[] | Probability distribution (0-100 scale) |
| initialYesPrices | bigint[] | (Optional) Initial Yes prices per option in 1e18 scale (e.g. 500000000000000000n = 50%). Defaults to 50% for both AMM and OrderBook |
| baseToken | 0x${string} | Base token address (USDT or RAIN) |
| tradingModel | TradingModel | AMM (0) or OrderBook (1) |
| marketImage | string | Market image URL (required) |
Constraints (validated, throws if violated):
no_of_options ≥ 2;marketOptions,barValues, and (if provided)initialYesPricesmust each have lengthno_of_options; everybarValuemust be within0–100and the set must sum to 100; eachinitialYesPricemust be within(0, 1e18)exclusive. Previously out-of-spec bar values were silently redistributed onto the last option — they are now rejected.
Trading
buildEnterOptionTx(params): Promise<RawTransaction[]>
Buy shares of an option (AMM trade). Automatically reads the market's base token, checks allowance, includes approval TX if needed, and calculates slippage protection via on-chain getEntryShares.
const txs = await rain.buildEnterOptionTx({
marketContractAddress: '0x...',
selectedOption: 1n, // 1-based option index
optionSide: OptionSide.Yes, // Yes = 1, No = 2
buyAmountInWei: parseUnits('5', 6), // 5 USDT (or parseUnits('5', 18) for RAIN)
walletAddress: '0x...', // user's wallet address
slippageTolerance: 5n, // optional, default 5% — percentage tolerance for minSharesOut
deadline: BigInt(Math.floor(Date.now() / 1000) + 600), // optional — absolute unix timestamp; omit for a default 10-min window
});
// Returns [approveTx?, enterOptionTx]| Parameter | Type | Description |
|-----------|------|-------------|
| marketContractAddress | 0x${string} | Market contract address |
| selectedOption | bigint | Option index (1-based) |
| optionSide | OptionSide | Yes (1) or No (2) |
| buyAmountInWei | bigint | Amount in base token wei |
| walletAddress | 0x${string} | User's wallet address (for allowance check) |
| minSharesOut | bigint | (Optional) Minimum shares to receive. Auto-calculated from getEntryShares with slippage if not set |
| slippageTolerance | bigint | (Optional) Slippage percentage (e.g. 5n = 5%). Default: 5% |
| deadline | bigint | (Optional) Absolute unix timestamp in seconds, e.g. BigInt(Math.floor(Date.now()/1000) + 600). Omit for a default 10-min window. A small duration like 600n is rejected (RainValidationError). |
Note: Approval is handled automatically. The SDK reads
baseTokenfrom the market contract and checks allowance before building transactions. Slippage protection is auto-calculated: the SDK callsgetEntryShareson-chain to get expected shares, then applies the slippage tolerance.
buildSellOptionTx(params): Promise<RawTransaction>
Market-sell shares of an option into the resting buy order book. No approval needed (you're selling shares, not tokens). Slippage protection is auto-calculated from on-chain getSellProceeds (default 5%) when minAmountOut is omitted. Pass minAmountOut: 0n to opt out explicitly.
const tx = await rain.buildSellOptionTx({
marketContractAddress: '0x...',
selectedOption: 1n, // 1-based option index
optionSide: OptionSide.Yes, // Yes = 1, No = 2
sharesAmount: parseUnits('10', 6), // shares to sell
slippageTolerance: 5n, // optional, default 5%
deadline: BigInt(Math.floor(Date.now() / 1000) + 600), // optional — absolute unix timestamp; omit for a default 10-min window
});
// Returns a single RawTransaction (no approval needed)| Parameter | Type | Description |
|-----------|------|-------------|
| marketContractAddress | 0x${string} | Market contract address |
| selectedOption | bigint | Option index (1-based) |
| optionSide | OptionSide | Yes (1) or No (2) |
| sharesAmount | bigint | Number of shares to sell |
| minAmountOut | bigint | (Optional) Minimum base tokens to receive. Auto-calculated from getSellProceeds with slippage if not set. Pass 0n to opt out |
| slippageTolerance | bigint | (Optional) Slippage percentage (e.g. 5n = 5%). Default: 5% |
| deadline | bigint | (Optional) Absolute unix timestamp in seconds, e.g. BigInt(Math.floor(Date.now()/1000) + 600). Omit for a default 10-min window. A small duration like 600n is rejected (RainValidationError). |
Split & Merge
buildSplitTx(params): Promise<RawTransaction[]>
Split base tokens into equal Yes + No shares for an option. Automatically checks allowance and includes approval TX if needed.
const txs = await rain.buildSplitTx({
marketContractAddress: '0x...',
option: 1n,
amount: parseUnits('5', 6), // 5 USDT -> 5 Yes shares + 5 No shares
walletAddress: '0x...', // user's wallet address
});
// Returns [approveTx?, splitTx]Note: Approval is handled automatically.
buildMergeTx(params: MergeTxParams): RawTransaction
Merge equal Yes + No share pairs back into base tokens.
const tx = rain.buildMergeTx({
marketContractAddress: '0x...',
option: 1n,
amount: 5000000n, // raw shares amount (from getUserOptionShares)
});Note: No approval needed. Burns your shares and returns base token.
Liquidity
buildAddLiquidityTx(params): Promise<RawTransaction[]>
Add liquidity to a specific option. Automatically checks allowance, includes approval TX if needed, and calculates slippage protection from on-chain AMM reserves.
const txs = await rain.buildAddLiquidityTx({
marketContractAddress: '0x...',
option: 1n,
totalAmountInWei: parseUnits('10', 6), // 10 USDT
walletAddress: '0x...', // user's wallet address
slippageTolerance: 5n, // optional, default 5%
deadline: BigInt(Math.floor(Date.now() / 1000) + 600), // optional — absolute unix timestamp; omit for a default 10-min window
});
// Returns [approveTx?, addLiquidityTx]| Parameter | Type | Description |
|-----------|------|-------------|
| marketContractAddress | 0x${string} | Market contract address |
| option | bigint | Option index (1-based) |
| totalAmountInWei | bigint | Amount in base token wei |
| walletAddress | 0x${string} | User's wallet address (for allowance check) |
| minYesToDeposit | bigint | (Optional) Min yes tokens to deposit. Auto-calculated from reserves if not set |
| minNoToDeposit | bigint | (Optional) Min no tokens to deposit. Auto-calculated from reserves if not set |
| slippageTolerance | bigint | (Optional) Slippage percentage (e.g. 5n = 5%). Default: 5% |
| deadline | bigint | (Optional) Absolute unix timestamp in seconds, e.g. BigInt(Math.floor(Date.now()/1000) + 600). Omit for a default 10-min window. A small duration like 600n is rejected (RainValidationError). |
Note: Approval is handled automatically. Slippage protection is auto-calculated from
ammYesReserve/ammNoReserveproportionally.
buildRemoveLiquidityTx(params: RemoveLiquidityTxParams): Promise<RawTransaction>
Remove liquidity by burning LP shares. Automatically calculates slippage protection via on-chain getRemovedLiquidity.
const lpShares = await rain.getUserOptionLPShares({
marketContractAddress: '0x...',
option: 1n,
userAddress: '0x...',
});
const tx = await rain.buildRemoveLiquidityTx({
marketContractAddress: '0x...',
option: 1n,
lpShares, // raw LP shares amount
slippageTolerance: 5n, // optional, default 5%
deadline: BigInt(Math.floor(Date.now() / 1000) + 600), // optional — absolute unix timestamp; omit for a default 10-min window
});| Parameter | Type | Description |
|-----------|------|-------------|
| marketContractAddress | 0x${string} | Market contract address |
| option | bigint | Option index (1-based) |
| lpShares | bigint | LP shares to remove |
| minYesOut | bigint | (Optional) Min yes tokens to receive. Auto-calculated from getRemovedLiquidity if not set |
| minNoOut | bigint | (Optional) Min no tokens to receive. Auto-calculated if not set |
| slippageTolerance | bigint | (Optional) Slippage percentage (e.g. 5n = 5%). Default: 5% |
| deadline | bigint | (Optional) Absolute unix timestamp in seconds, e.g. BigInt(Math.floor(Date.now()/1000) + 600). Omit for a default 10-min window. A small duration like 600n is rejected (RainValidationError). |
Order Book
buildPlaceBuyOrderTx(params): Promise<RawTransaction[]>
Place a limit buy order. Automatically checks allowance and includes approval TX if needed.
const txs = await rain.buildPlaceBuyOrderTx({
marketContractAddress: '0x...',
option: 1n,
optionSide: OptionSide.Yes,
price: parseEther('0.5'), // 50% price in 1e18 scale
amount: parseUnits('10', 6), // 10 USDT
postOnly: false, // optional, defaults to false. Set true for maker-only (reverts OrderWouldCross if it would cross the book)
walletAddress: '0x...', // user's wallet address
});
// Returns [approveTx?, placeBuyOrderTx]Note: Approval is handled automatically. Available when AMM pool is closed.
postOnly: Whentrue, the matching loop is skipped — the order rests directly. Ifpricewould cross a resting ask the call revertsOrderWouldCross, so the caller is never charged taker fees. Whenfalse(default), behaviour is unchanged: matches the sell book first and rests only the residual.
buildPlaceSellOrderTx(params: PlaceSellOrderTxParams): RawTransaction
Place a limit sell order.
const tx = rain.buildPlaceSellOrderTx({
marketContractAddress: '0x...',
option: 1n,
optionSide: OptionSide.Yes,
price: parseEther('0.7'), // 70% price in 1e18 scale
shares: 5000000n, // shares to sell (from getUserOptionShares)
postOnly: false, // optional, defaults to false. Set true for maker-only (reverts OrderWouldCross if it would cross the book)
});Note: No approval needed. Shares are escrowed by the contract.
postOnly: Same semantics asbuildPlaceBuyOrderTx—truerejects orders that would immediately cross the buy book (revertOrderWouldCross);falsematches the buy book first and rests only the residual.
buildCancelBuyOrdersTx(params: CancelBuyOrdersTxParams): RawTransaction
Cancel one or more buy orders.
const tx = rain.buildCancelBuyOrdersTx({
marketContractAddress: '0x...',
option: 1n,
optionSides: [1, 1], // Yes, Yes
prices: [parseEther('0.5'), parseEther('0.6')],
orderIDs: [1n, 2n], // from PlaceBuyOrder event
});buildCancelSellOrdersTx(params: CancelSellOrdersTxParams): RawTransaction
Cancel one or more sell orders. Same interface as buildCancelBuyOrdersTx.
Market Resolution
buildClosePoolAITx(params: ClosePoolAITxParams): Promise<RawTransaction[]>
Close a market pool with AI resolver. Returns approval (for resolver bond) + closePool TX.
const txs = await rain.buildClosePoolAITx({
marketContractAddress: '0x...',
option: 1n,
});buildClosePoolManualTx(params: ClosePoolManualTxParams): Promise<RawTransaction[]>
Close a market pool with manual resolver, proposing a winner side. Returns approval (for resolver bond) + closePool TX.
const txs = await rain.buildClosePoolManualTx({
marketContractAddress: '0x...',
option: 1n,
proposedWinner: 1, // 1 = Yes, 2 = No
});buildChooseWinnerTx(params: ChooseWinnerTxParams): RawTransaction
Finalize the winner after pool is closed.
const tx = rain.buildChooseWinnerTx({
marketContractAddress: '0x...',
option: 1n,
optionSide: 1, // 1 = Yes, 2 = No
});buildCalculateWinnerTx(params: CalculateWinnerTxParams): Promise<RawTransaction>
Calculate winner via the oracle resolver. Must be called before claim. Reads optionResolver(option) from the market and calls calculateWinner() on that resolver contract.
const tx = await rain.buildCalculateWinnerTx({
marketContractAddress: '0x...',
option: 1n,
});buildExtendTimeTx(params: ExtendTimeTxParams): Promise<RawTransaction>
Extend the resolution time of a market option. Requires a signature from the oracle backend (obtained via signOraclesExtendTime API). Reads optionResolver(option) from the market and calls extendTime(newEndTime, signature) on that resolver contract.
// 1. Get the signature from the API
const signResult = await rain.signOraclesExtendTime(
{ contractAddress: '0x...', walletAddress: '0x...' },
accessToken
);
// 2. Build the extend time transaction
const tx = await rain.buildExtendTimeTx({
marketContractAddress: '0x...',
option: 1n,
newEndTime: BigInt(Math.floor(Date.now() / 1000) + 86400), // extend by 1 day
signature: signResult.signature, // from API
});| Param | Type | Description |
|---|---|---|
| marketContractAddress | 0x${string} | Market contract address |
| option | bigint | Option index (1-based) |
| newEndTime | bigint | New end time as unix timestamp (seconds) |
| signature | 0x${string} | Backend signature from signOraclesExtendTime |
Claiming
buildClaimTx(params: ClaimTxParams): RawTransaction
Claim winnings after market resolution. Call buildCalculateWinnerTx first.
const tx = rain.buildClaimTx({
marketContractAddress: '0x...',
option: 1n,
});Dispute / Appeal
buildOpenDisputeTx(params: OpenDisputeTxParams): Promise<RawTransaction[]>
Open a dispute (or appeal if dispute already exists). Returns approval (for dispute fee) + openDispute TX. The fee is read from getDisputeAppealFee(option).
const txs = await rain.buildOpenDisputeTx({
marketContractAddress: '0x...',
option: 1n,
});Note: Call once to open dispute. Call again (after dispute window) to appeal.
Token Approval
buildApprovalTx(params: ApproveTxParams): RawTransaction
Build an ERC20 approve transaction.
const tx = rain.buildApprovalTx({
tokenAddress: config.tokens.usdt.address,
spender: '0x...', // market contract address
amount: parseUnits('100', 6), // 100 USDT
});Read Methods (On-Chain Queries)
getTokenAllowance(params): Promise<bigint>
Check current ERC20 allowance.
const allowance = await rain.getTokenAllowance({
tokenAddress: config.tokens.usdt.address,
owner: '0x...', // your address
spender: '0x...', // market contract
});getUserOptionLPShares(params): Promise<bigint>
Get user's LP shares for a specific option.
const lpShares = await rain.getUserOptionLPShares({
marketContractAddress: '0x...',
option: 1n,
userAddress: '0x...',
});getUserOptionShares(params): Promise<bigint>
Get user's Yes or No shares for a specific option.
const yesShares = await rain.getUserOptionShares({
marketContractAddress: '0x...',
option: 1n,
optionSide: 1, // 1 = Yes, 2 = No
userAddress: '0x...',
});getOptionClaimed(params): Promise<boolean>
Check if a user has already claimed winnings for a specific option.
const claimed = await rain.getOptionClaimed({
marketContractAddress: '0x...',
option: 1n,
userAddress: '0x...',
});
// true if already claimed, false otherwisegetEntryShares(params): Promise<{ returnedShares: bigint; expectedReward: bigint }>
Calculate how many shares you'd receive for a given buy amount, plus the expected reward.
const result = await rain.getEntryShares({
marketContractAddress: '0x...',
option: 1n, // 1-based option index
optionSide: 1, // 1 = Yes, 2 = No
amount: 1000000n, // buy amount in base token wei
});
// result.returnedShares = shares you'd receive
// result.expectedReward = expected reward amountgetClaimableAmount(params): Promise<bigint>
Get the total claimable amount for a user on a specific option.
const claimable = await rain.getClaimableAmount({
marketContractAddress: '0x...',
userAddress: '0x...',
option: 1n, // 1-based option index
});
// claimable = total claimable amount in base token weigetSellProceeds(params): Promise<{ proceeds: bigint; sharesSold: bigint }>
Get the expected proceeds and shares sold for selling shares of an option.
const result = await rain.getSellProceeds({
marketContractAddress: '0x...',
option: 1n, // 1-based option index
optionSide: 1, // 1 = Yes, 2 = No
shares: 1000000n, // shares to sell (raw amount)
});
// result.proceeds = base token amount you'd receive
// result.sharesSold = shares that would actually be soldgetDisputeWindow(params): Promise<bigint>
Get the dispute window duration (in seconds) for a market contract.
const window = await rain.getDisputeWindow({
marketContractAddress: '0x...',
});
// window = dispute window in seconds (e.g. 7200n = 2 hours)getDynamicPayout(params): Promise<bigint[]>
Get the dynamic payout amounts for a user on a specific option. Returns an array of payout values per side.
const payouts = await rain.getDynamicPayout({
marketContractAddress: '0x...',
userAddress: '0x...',
option: 1n, // 1-based option index
});
// payouts = [yesPayoutWei, noPayoutWei]getResolverBondAmount(params): Promise<bigint>
Get the bond amount (in base-token wei) a resolver must post to propose a resolution for an option.
const bond = await rain.getResolverBondAmount({
marketContractAddress: '0x...',
option: 1n, // 1-based option index
});getDisputeAppealFee(params): Promise<bigint>
Get the fee (in base-token wei) required to open a dispute or appeal on an option.
const fee = await rain.getDisputeAppealFee({
marketContractAddress: '0x...',
option: 1n,
});optionResolverBond(params): Promise<bigint>
Get the resolver bond currently posted on an option (0n if no resolution has been proposed).
const bond = await rain.optionResolverBond({
marketContractAddress: '0x...',
option: 1n,
});optionResolutionProposer(params): Promise<0x${string}>
Get the address that proposed the resolution for an option (zero address if none).
const proposer = await rain.optionResolutionProposer({
marketContractAddress: '0x...',
option: 1n,
});isOptionDisputed(params): Promise<boolean>
Check whether an option's resolution has been disputed.
const disputed = await rain.isOptionDisputed({
marketContractAddress: '0x...',
option: 1n,
});isOptionAppealed(params): Promise<boolean>
Check whether an option's dispute resolution has been appealed.
const appealed = await rain.isOptionAppealed({
marketContractAddress: '0x...',
option: 1n,
});optionDispute(params): Promise<OptionDisputeInfo>
Get the dispute details for an option: fee paid, the winner claimed by the disputer, the disputer address, and the dispute resolver.
const dispute = await rain.optionDispute({
marketContractAddress: '0x...',
option: 1n,
});
// { disputeFee: bigint, disputedWinner: bigint, disputer: '0x...', disputeResolver: '0x...' }optionAppeal(params): Promise<OptionAppealInfo>
Get the appeal details for an option: fee paid, the winner claimed by the appellant, the disputer address, and the appeal resolver.
const appeal = await rain.optionAppeal({
marketContractAddress: '0x...',
option: 1n,
});
// { disputeFee: bigint, disputedWinner: bigint, disputer: '0x...', appealResolver: '0x...' }getUserActiveBuyOrders(params): Promise<bigint>
Get count of user's active buy orders.
const count = await rain.getUserActiveBuyOrders({
marketContractAddress: '0x...',
userAddress: '0x...',
});getUserActiveSellOrders(params): Promise<bigint>
Get count of user's active sell orders.
const count = await rain.getUserActiveSellOrders({
marketContractAddress: '0x...',
userAddress: '0x...',
});getFirstBuyOrderPrice(params): Promise<bigint>
Get the best (first) buy order price for an option/side.
const price = await rain.getFirstBuyOrderPrice({
marketContractAddress: '0x...',
option: 1n,
optionSide: 1, // Yes
});
// price is in 1e18 scale. 500000000000000000n = 0.5 = 50%getFirstSellOrderPrice(params): Promise<bigint>
Get the best (first) sell order price for an option/side.
getBuyOrdersAtPrice(params): Promise<OrderLevelInfo>
Get order book info at a specific price level.
const info = await rain.getBuyOrdersAtPrice({
marketContractAddress: '0x...',
option: 1n,
optionSide: 1,
price: parseEther('0.5'),
});
// { headIndex, tailIndex, count, isInitialized }getSellOrdersAtPrice(params): Promise<OrderLevelInfo>
Same as above for sell orders.
checkOrderExists(params): Promise<{ exists: boolean; index: bigint }>
Check if a specific order exists.
const result = await rain.checkOrderExists({
marketContractAddress: '0x...',
option: 1n,
optionSide: 1,
price: parseEther('0.5'),
orderID: 1n,
});Authentication
login(params: LoginParams): Promise<LoginResult>
Wallet-based login to the Rain backend.
signLoginMessage(walletClient, walletAddress)
Signs the lowercased wallet address using personal_sign. This signature is required for login.
import { signLoginMessage } from 'rain-sdk-v2';
const walletClient = createWalletClient({ chain: arbitrum, transport: custom(window.ethereum) });
const signature = await signLoginMessage(walletClient, '0x...' as `0x${string}`);login(params)
const result = await rain.login({
signature, // from signLoginMessage
walletAddress: '0x...', // EOA address
smartWalletAddress: '0x...', // optional — defaults to walletAddress for plain-EOA integrations
referredBy: 'CODE', // optional referral code
});
console.log(result.accessToken); // JWT token
console.log(result.userId); // Backend user IDConfiguration
getEnvironmentConfig()
Returns the current environment configuration.
const config = rain.getEnvironmentConfig();Returns:
| Field | Description |
|-------|-------------|
| apiUrl | Backend API URL |
| market_factory_address | Factory contract address |
| dispute_initial_timer | Dispute timer in seconds |
| tokens.usdt | USDT token config (address, symbol, decimals, oracle_fixed_fee_per_option) |
| tokens.rain | RAIN token config (address, symbol, decimals, oracle_fixed_fee_per_option) |
getTokenConfig(tokenAddress)
Looks up token configuration by contract address. Returns TokenConfig or null.
const tokenInfo = rain.getTokenConfig('0x...');
// { address, symbol, decimals, oracle_fixed_fee_per_option }Enums
TradingModel
enum TradingModel {
AMM = 0,
OrderBook = 1,
}OptionSide
enum OptionSide {
Yes = 1,
No = 2,
}Environments
| Environment | API | Factory | USDT | RAIN |
|-------------|-----|---------|------|------|
| development | https://dev2-api.rain.one | 0xbbDd...f02f | 6 decimals | 18 decimals |
| stage | https://stg2-api.rain.one | 0x16cc...628d | 6 decimals | 18 decimals |
| production | https://prod2-api.rain.one | 0x38B3...C677 | 6 decimals | 18 decimals |
Supported Tokens
Each environment includes token configs for both USDT (6 decimals) and RAIN (18 decimals). The SDK automatically handles decimal conversions for oracle fees and minimum liquidity validation based on the token used.
const config = rain.getEnvironmentConfig();
// USDT
config.tokens.usdt.address // Token contract address
config.tokens.usdt.decimals // 6
config.tokens.usdt.symbol // "USDTm" (dev) or "USD₮0" (stage/production)
// RAIN
config.tokens.rain.address // Token contract address
config.tokens.rain.decimals // 18
config.tokens.rain.symbol // "RAIN"Typical Flow
1. Create Market (buildCreateMarketTx)
2. Wait for startTime to pass
3. Enter Option (buildEnterOptionTx) / Split (buildSplitTx)
4. Place Orders (buildPlaceBuyOrderTx / buildPlaceSellOrderTx)
5. Wait for endTime to pass
6. Close Pool (buildClosePoolAITx / buildClosePoolManualTx)
7. Calculate Winner (buildCalculateWinnerTx)
8. Check if claimed (getOptionClaimed) — optional
9. Claim (buildClaimTx)If disputed:
6b. Open Dispute (buildOpenDisputeTx)
6c. Appeal (buildOpenDisputeTx again)If extending time:
5b. Extend Time (signOraclesExtendTime API → buildExtendTimeTx)REST API Methods
The SDK wraps all Rain backend REST API endpoints. Each method requires accessToken for authenticated routes.
Users
// Find user by wallet address (public)
const user = await rain.findUserByWalletAddress({ walletAddress: '0x...' });
// Get authenticated user's profile
const profile = await rain.getUserProfile(accessToken);
// View another user's profile
const profile = await rain.viewUserProfile({ userId: '...' }, accessToken);
// Update profile
await rain.updateUserProfile({
name: 'John',
bio: 'Trader',
profilePic: 'https://...',
twitterLink: 'https://...',
}, accessToken);
// Get total user count (public)
const count = await rain.getUsersTotalCount();
// Remove profile picture
await rain.removeUserProfilePic(accessToken);
// Get user activity history (deposits, withdrawals, trades, rewards)
const history = await rain.getUserHistory({ limit: 20, offset: 1 }, accessToken);
// Check if access token is still valid
const status = await rain.checkTokenExpiration(accessToken);Comments
// Create a comment
await rain.createComment({ comment: 'Great market!', poolId: '...' }, accessToken);
// Reply to a comment
await rain.createComment({ comment: 'I agree', poolId: '...', parentCommentId: '...' }, accessToken);
// Get comments for a pool
const comments = await rain.getCommentsListing({ poolId: '...', limit: 10, offset: 0 });
// Update a comment
await rain.updateComment({ commentId: '...', comment: 'Updated text' }, accessToken);
// Like / Unlike a comment
await rain.likeComment({ commentId: '...' }, accessToken);
await rain.unlikeComment({ commentId: '...' }, accessToken);
// Get comment count for a pool
const count = await rain.getCommentsCount({ poolId: '...' });Pools
// Get public pools (paginated, filterable)
const pools = await rain.getPublicPools({ limit: 10, offset: 0, status: 'Live', sortBy: 'trending' });
// Get private pools
const pools = await rain.getPrivatePools({ limit: 10, offset: 0 }, accessToken);
// Get pool by ID
const pool = await rain.getPoolById({ id: '...' });
// Get pool by contract address
const pool = await rain.getPoolByContractAddress({ contractAddress: '0x...' });
// Search pools
const results = await rain.searchPool({ query: 'bitcoin', limit: 10, offset: 0 });
// Get trending tags (ranked by market activity in last 24h)
const tags = await rain.getTrendingTags({ limit: 10 });
// Get featured pools
const featured = await rain.getFeaturedPools({ limit: 10, offset: 0 });
// Get all pools count
const count = await rain.getAllPoolsCount();
// Get related pools
const related = await rain.getRelatedPools({ poolId: '...' });
// Get pool resolution history (per sub-pool)
const history = await rain.getPoolResolutionHistory({ poolId: '...', subPool: '...' });
// Access a private pool
const access = await rain.accessPool({ poolId: '...', accessCode: '1234' });
// Verify access code
await rain.verifyAccessCode({ poolId: '...', accessCode: '1234' }, accessToken);
// Get pools created by a user
const pools = await rain.getPoolListingByCreator({ limit: 10, offset: 0 }, accessToken);
// Get total participants in a pool
const count = await rain.getPoolTotalParticipants({ poolId: '...' });
// Get total pools created by user
const count = await rain.getTotalPoolsCreatedByUser(accessToken);
// Get total predictions by user
const count = await rain.getTotalPredictionsByUser(accessToken);
// Update streaming status
await rain.updateStreaming({ poolId: '...', streaming: true }, accessToken);
// Update pool resolution time
await rain.updatePoolResolutionTime({ poolId: '...', newEndDate: '...' }, accessToken);
// Find pool fallback
const pool = await rain.findPoolFallback({ poolId: '...' });
// Sign oracles extend time
await rain.signOraclesExtendTime({ poolId: '...' }, accessToken);Investments
// Get user's total investment in a pool
const investment = await rain.getUserTotalInvestment({ poolId: '...' }, accessToken);
// Get options total volume for a pool
const volume = await rain.getOptionsTotalVolume({ poolId: '...' });
// Get pool activity (trades)
const activity = await rain.getPoolActivity({ poolId: '...', limit: 10, offset: 0 });
// Get top holders in a pool
const holders = await rain.getTopHolders({ poolId: '...', limit: 10, offset: 0 });
// Get user's invested pools
const pools = await rain.getUserInvestedPools({ limit: 10, offset: 0 }, accessToken);
// Get platform TVL
const tvl = await rain.getPlatformTvl();
// Get investment volume graph
const graph = await rain.getInvestmentVolumeGraph({ period: '7d' }, accessToken);
// Calculate user PNL
const pnl = await rain.calculateUserPnl({ walletAddress: '0x...' }, accessToken);
// Get user total volume
const volume = await rain.calculateUserTotalVolume(accessToken);
// Get PNL graph
const graph = await rain.getUserPnlGraph({ period: '30d' }, accessToken);
// Get TVL graph
const graph = await rain.getTvlGraph();
// Get PNL per pool
const pnl = await rain.calculateUserPnlPerPool(accessToken);
// Get PNL by specific pool
const pnl = await rain.getPnlByPoolId({ poolId: '...' }, accessToken);
// Get top winners and losers
const leaderboard = await rain.getTopWinnersLosers({ limit: 10, period: '7d' });
// Get user overall investment
const overall = await rain.getUserOverallInvestment(accessToken);
// Get platform volume
const volume = await rain.getPlatformVolume();
// Get user invested live pools count
const count = await rain.getUserInvestedLivePoolsCount(accessToken);
// Calculate user open interest
const oi = await rain.calculateUserOpenInterest(accessToken);
// Get the authenticated user's positions in a specific subPool
// (per-side net shares, weighted average buy price, projected payout on win)
const positions = await rain.getUserPositions(
{ poolId: '...', subPoolIndex: 1 },
accessToken
);
// Get the authenticated user's open positions across all subPools of a pool
// (one entry per (subPool, side) the user still holds; excludes claimed / losing sides)
const open = await rain.getOpenPositions({ poolId: '...' }, accessToken);
// Get user's open share positions per side with price move vs avg buy price
const sharePositions = await rain.getUserSharePositions({ poolId: '...' }, accessToken);
// Search user's invested pools by keyword (matched against question and tags)
const results = await rain.searchInvestedPools({
question: 'world cup', // required — keyword to match
status: 'active', // optional — 'all' | 'active' | 'closed' | granular status
sortBy: 'value', // optional — 'value' (net amount) | 'recent' (creation time)
limit: 10, // optional — max results (default 10)
}, accessToken);Orders
// Get user's orders
const orders = await rain.getUserOrders({ limit: 10, offset: 1, filter: 'pending' }, accessToken);
// Get order book for a pool
const book = await rain.getOrderBook({ pool: '...' });
// Get user's orders for a specific pool
const orders = await rain.getUserOrderByPoolId({ poolId: '...' }, accessToken);
// Get all orders for a pool
const orders = await rain.getOrdersListingByPool({ pool: '...', limit: 10, offset: 1 });
// Get order by ID
const order = await rain.getOrderById({ orderId: '...' });Points
// Get user points
const points = await rain.getUserPoints(accessToken);
// Get user points graph
const graph = await rain.getUserPointsGraph(accessToken);
// Add user points
await rain.addUserPoints({ points: 100, reason: '...' }, accessToken);
// User successful onboarding
await rain.userSuccessfulOnboarding({ step: '...' }, accessToken);Notifications
// Get notifications
const notifs = await rain.getNotifications({ limit: 20, offset: 0 }, accessToken);
// Mark all as read
await rain.markAllNotificationsAsRead(accessToken);
// Mark single notification as read
await rain.markNotificationAsRead({ notificationId: '...' }, accessToken);Price Data
// Get price data for a pool
const prices = await rain.getPriceData({
contractAddress: '0x...', // market contract address
side: 1, // 1 = YES, 2 = NO
filter: '1D', // '1H' | '6H' | '1D' | '1W' | '1M' | 'ALL'
});Pool Reviews
// Add review
await rain.addReview({ poolId: '...', rating: 5, review: 'Great!' }, accessToken);
// Get user's reviews
const reviews = await rain.getUserReviews(accessToken);
// Get pools by creator with reviews
const pools = await rain.getPoolsByCreatorReviews(accessToken);Follow
// Toggle follow a user
await rain.toggleFollow({ userId: '...' }, accessToken);
// Check if following
const status = await rain.checkFollow({ userId: '...' }, accessToken);
// Get followers
const followers = await rain.getFollowers({ userId: '...', limit: 20, offset: 0 });
// Get following
const following = await rain.getFollowing({ userId: '...', limit: 20, offset: 0 });
// Get follow stats
const stats = await rain.getFollowStats({ userId: '...' });Rain Burn
// Get total RAIN burned
const burned = await rain.getTotalBurned();
// Get burn per pool
const burn = await rain.getBurnPerPool({ poolId: '...' });Dispute
// Create dispute message (text)
await rain.createDisputeMessage({
pool: '...', // Parent pool document ID
subPool: '...', // SubPool document ID
role: 'disputer', // 'creator' | 'disputer' | 'proposer'
messageType: 'text', // 'text' | 'image' | 'video' | 'file' | 'youtube' | 'mixed'
evidence: {
options: ['Yes', 'No'], // required, at least 2
evidenceType: 'photo', // 'photo' | 'video' | 'pdf' | 'youtube' | 'mixed'
question: 'Did the event happen?', // optional
description: 'Screenshot of result', // optional
source: 'https://espn.com/result', // optional
youtubeUrls: ['https://youtube.com/watch?v=abc123'], // optional
urls: ['https://example.com/proof'], // optional
},
}, accessToken);
// Create dispute message with file attachments (image/video/pdf)
await rain.createDisputeMessage({
pool: '...',
subPool: '...',
role: 'creator',
messageType: 'image', // set to 'file' for PDF
files: [fileObject], // File[] — required for image/video/file/mixed types, max 25MB each
evidence: {
options: ['Yes', 'No'],
evidenceType: 'photo', // use 'pdf' for PDF files
},
}, accessToken);
// Get dispute conversation for a sub-pool
const convo = await rain.getPoolDisputeConvo({ poolId: '...', subPool: '...', limit: 50, offset: 0 }, accessToken);Bookmarks
// Toggle bookmark (add/remove) for a market
await rain.toggleBookmark({ poolId: '...' }, accessToken);
// Get user's bookmarked markets (paginated)
const bookmarks = await rain.getBookmarks({ page: 1, limit: 20 }, accessToken);
// Check if a market is bookmarked
const status = await rain.checkBookmark({ poolId: '...' }, accessToken);Whitelisted Tokens
// Get token price in USD
const priceData = await rain.getTokenPrice('0x...'); // token contract address
// priceData.data = { price: 0.05 } — USD price per tokenLeaderboard
Public endpoints — no access token required. Boards are served from a materialized document rebuilt by a cron (~20 min), so changes appear after the next tick rather than on the next request.
// Get a ranked leaderboard — one board per (board, window, category)
const board = await rain.getLeaderboard({
board: 'profit', // 'profit' (net realized PnL) or 'correct_calls' (sum of 1 − entryPrice over winning positions)
window: 'all_time', // 'all_time' (default) or 'monthly' (current UTC calendar month)
category: 'Crypto', // optional, case-insensitive; omit for the global board
limit: 10, // up to the stored cap of 100 (default 10)
});
// board.data = { board, window, category, categoryLabel, period, limit, totalRanked, priceBasis, entries, lastUpdatedAt }
// Each entry carries all four table columns — profit, correctCalls, winRate, trades —
// whichever board is sorting, so switching the sorted column needs no second request.
// Search traders on a board — the leaderboard's trader search box
const found = await rain.searchLeaderboard({
q: '0x742d', // wallet address, address fragment (with/without 0x, prefix/middle/tail), or user id
board: 'profit', // required — searches the stored board for this (board, window, category)
window: 'all_time', // optional, default 'all_time'
category: 'Crypto', // optional; omit for the global board
limit: 10, // up to 50 (default 10)
});
// found.data = { board, window, category, period, query, matched, searchedTop, totalRanked, lastUpdatedAt, results }
// `results` are the same entry objects getLeaderboard returns, in rank order.
// Scoped to the stored board — a trader outside the top N is simply not found.
// Empty `results` means "not on this board"; `searchedTop` + `totalRanked` let you
// say "not in the top 100" precisely. Queries under 2 characters return empty, not a 400.
// Get a trader's most recent trades — the rows inside an expanded leaderboard row
const recent = await rain.getTraderRecentTrades({
userId: '...', // from a leaderboard entry's `userId`
limit: 5, // rendered rows (a multi-side trade flattens into more lines), up to 25 (default 5)
});
// recent.data = { userId, limit, trades }
// Each trade: { action ('Buy'|'Sell'), origin, transactionHash, tradedAt, poolId, question,
// poolImage, subPoolId, subQuestion, side (1=YES, 2=NO), optionName, shares,
// amountUSD, pricePerShare, priceCents, status ('open'|'pending'|'settled'),
// pnlScope, pnlUSD }
// Directional trades only (origin ∈ {enter, orderFill}), newest first — and unlike the
// track-record endpoint, OPEN positions are included (status 'open').
// `pnlUSD` is POSITION-level, not per-fill: several trades in the same market report the
// same number. Null until settled; `amountUSD`/`pnlUSD` are dollars × 1e6.
// Get categories that currently have a ranked board (feeds the category filter)
const categories = await rain.getLeaderboardCategories();
// categories.data = [{ category: 'CRYPTO', label: 'Crypto' }, ...]
// Pass `category` back as the `category` param of getLeaderboard.Notes:
- Units: every
*USDnumber (includingprofitandscoreon the profit board) is dollars × 1e6 — divide by 1,000,000 to display. E.g.profit: 887505347is $887.51. - Monthly window buckets on when PnL settled, not when the position was opened;
window: 'monthly'always means the current UTC month (periodreports it asYYYY-MM). - Only positive scores are ranked. An unknown
categoryreturns an empty board, not a 400. - Top-N only — there is no rank-of-user lookup. Empty
entrieswithlastUpdatedAt: nullmeans the cron hasn't built the board yet; empty with a timestamp means nobody qualifies. - The client can highlight the viewer's own row by matching
userIdinentries.
WebSocket Events (Socket.IO)
Real-time event subscriptions via Socket.IO.
Setup
import { RainSocket } from 'rain-sdk-v2';
const socket = new RainSocket('https://dev-api.rain.one');
socket.connect();Pool-Scoped Events
Subscribe to events for a specific pool using poolId:
// Trading events
const unsub = socket.onEnterOption(poolId, (data) => {
console.log(data.enterOption, data.pool, data.subPool);
});
socket.onExitOption(poolId, (data) => { /* data.exitOption, pool, subPool */ });
socket.onLiquidity(poolId, (data) => { /* data.enterLiquidity, pool, subPool */ });
socket.onSplit(poolId, (data) => { /* data.split, pool, subPool */ });
socket.onMerge(poolId, (data) => { /* data.merge, pool, subPool */ });
socket.onRemoveLiquidity(poolId, (data) => { /* data.removeLiquidity, pool, subPool */ });
// Price updates
socket.onSyncPrice(poolId, (data) => {
// data.prices = [{ side: 1, price: 0.62, percentage: 62, subPoolIndex: 1 }, ...]
// data.pool, data.subPool
});
// Order book events
socket.onOrderCreated(poolId, (data) => { /* data.order, pool, subPool */ });
socket.onOrderCancelled(poolId, (data) => { /* data.order, pool, subPool */ });
socket.onOrderFilled(poolId, (data) => { /* data.filledOrder, data.pendingOrder?, pool, subPool */ });
// Pool lifecycle
socket.onPoolClosed(poolId, (data) => { /* data.pool, data.subPool? */ });
socket.onPoolReverted(poolId, (data) => { /* data.pool, data.subPool */ });
socket.onPoolTokenSet(poolId, (data) => { /* data.pool */ });
socket.onStreamingStatusChanged(poolId, (data) => { /* data.pool */ });
// Resolution events
socket.onWinner(poolId, (data) => { /* data.pool, data.subPool */ });
socket.onWinnerProposer(poolId, (data) => { /* data.pool, data.subPool */ });
socket.onRevealWinnerAvailable(poolId, (data) => { /* data.subPoolId */ });
// Dispute & Appeal events
socket.onDisputeOpened(poolId, (data) => { /* data.subPool, data.eventType */ });
socket.onOracleCreated(poolId, (data) => { /* data.pool, data.subPool */ });
socket.onDisputeTimeExtended(poolId, (data) => { /* data.pool, data.subPool */ });
socket.onAppealOpened(poolId, (data) => { /* data.pool, data.subPool */ });
socket.onAppealWinnerCalculated(poolId, (data) => { /* data.subPool */ });
socket.onDisputeWinner(poolId, (data) => { /* data.subPool, data.eventType */ });
socket.onAppealWinner(poolId, (data) => { /* data.subPool, data.eventType, data.winnerFinalized */ });
// Claim events (pool-scoped broadcast)
socket.onClaimReward(poolId, (data) => { /* data.claimReward, pool, subPool */ });User-Scoped Events
Events targeted to a specific user (require userId):
// Personal claim reward
socket.onUserClaimReward(poolId, userId, (data) => { /* data.claimReward, pool, subPool */ });
// Dispute bond refunded (disputer won)
socket.onDisputeRefund(poolId, userId, (data) => { /* data.claimReward, pool, subPool */ });
// Appeal fee refunded (appealer won)
socket.onAppealRefund(poolId, userId, (data) => { /* data.claimReward, pool, subPool */ });
// Resolution bond refunded (proposer was correct)
socket.onResolutionRefund(poolId, userId, (data) => { /* data.claimReward, pool, subPool */ });
// Resolver closing share reward
socket.onResolverReward(poolId, userId, (data) => { /* data.claimReward, pool, subPool */ });
// Personal notifications
socket.onNotifications(userId, (data) => {
// data._id, userId, type, message, pool, subPool, createdAt, read
});Global Events
// New pool created
socket.onNewPool((data) => { /* data.pool, data.subMarkets */ });Generic Subscription
// Subscribe to any channel directly
const unsub = socket.onChannel('custom-event/123', (data) => { ... });
// Unsubscribe
unsub();Cleanup
socket.disconnect();All Socket Event Channels
| Channel | Description |
|---------|-------------|
| pool | New pool created |
| pool-token-set/{poolId} | Token set for pool |
| pool-closed/{poolId} | Pool/SubPool finalized |
| pool-reverted/{poolId} | Resolution reverted |
| streamingStatusChanged/{poolId} | Streaming toggled |
| enter-option/{poolId} | Shares bought (AMM or order match) |
| exit-option/{poolId} | Shares sold (order match) |
| liquidity/{poolId} | Liquidity added |
| split/{poolId} | Tokens split into Yes+No |
| merge/{poolId} | Yes+No merged back |
| remove-liquidity/{poolId} | Liquidity removed |
| sync-price/{poolId} | Price update |
| order-created/{poolId} | New order placed |
| order-cancelled/{poolId} | Order cancelled |
| order-filled/{poolId} | Order filled (full/partial) |
| winner/{poolId} | Winner proposed |
| winner-proposer/{poolId} | Proposer recorded |
| reveal-winner-available/{poolId} | Appeal winner ready |
| dispute-opened/{poolId} | Dispute opened |
| oracle-created/{poolId} | Dispute oracle deployed |
| dispute-time-extented/{poolId} | Deadline extended |
| appeal-opened/{poolId} | Appeal filed |
| appeal-winner-calculated/{poolId} | Oracle calculated winner |
| dispute-winner/{poolId} | Dispute decided (LEX) |
| appeal-winner/{poolId} | Final appeal winner |
| claim-reward/{poolId} | Reward claimed (broadcast) |
| claim-reward/{poolId}/{userId} | Reward claimed (personal) |
| dispute-refund/{poolId}/{userId} | Dispute bond refunded |
| appeal-refund/{poolId}/{userId} | Appeal fee refunded |
| resolution-refund/{poolId}/{userId} | Resolution bond refunded |
| resolver-reward/{poolId}/{userId} | Resolver reward |
| notifications/{userId} | Personal notifications |
Helpers & Execution
Executing transactions
Builders return unsigned RawTransaction[] where index 0 may be an ERC-20
approval. rain.execute() sends them in order and waits for each receipt before
the next — so the main transaction never front-runs its approval:
const txs = await rain.buildEnterOptionTx({ /* ... */ });
await rain.execute(txs, {
send: (tx) => walletClient.sendTransaction({ to: tx.to, data: tx.data, value: tx.value ?? 0n }),
wait: (hash) => publicClient.waitForTransactionReceipt({ hash }),
});The executor is pluggable — adapt { send, wait? } to viem, ethers, RainAA, or
your own KMS. Also available as the standalone executeTxs(txs, executor).
Amount conversion
Convert between human-readable amounts and base units using the token's configured decimals — no need to track that USDT is 6 and RAIN is 18:
rain.parseAmount('5', usdtAddress); // 5_000_000n
rain.formatAmount(5_000_000n, usdtAddress); // '5'Typed pool data
getPublicPools / getPrivatePools return ApiResponse<PaginatedPools>, and
socket event payloads type their pool / subPool fields as Pool / SubPool.
Import the DTOs to annotate your own code:
import type { Pool, SubPool, PaginatedPools } from 'rain-sdk-v2';Socket event names
Use RAIN_SOCKET_EVENTS for correctly-spelled event identifiers with the
low-level .on() API (some raw wire names are misspelled upstream):
import { RAIN_SOCKET_EVENTS } from 'rain-sdk-v2';
socket.on(RAIN_SOCKET_EVENTS.disputeTimeExtended, poolId, handler);Token expiry
login() returns expiresAt (unix seconds, decoded from the JWT) so you can
renew before it lapses:
const { accessToken, expiresAt } = await rain.login({ /* ... */ });Error Handling & Validation
Build-time validation
Transaction builders validate their inputs synchronously — before any encoding
or broadcast — and throw RainValidationError on the common footguns:
deadlinemust be an absolute unix timestamp (seconds), not a duration.deadline: 600nis rejected; useBigInt(Math.floor(Date.now()/1000) + 600).slippageToleranceis a whole percent (5n= 5%), and must be0–100.option/selectedOptionare 1-based (the first option is1, never0).
import { RainValidationError } from 'rain-sdk-v2';
try {
await rain.buildEnterOptionTx({ /* ... */, deadline: 600n });
} catch (err) {
if (err instanceof RainValidationError) {
console.error(err.field, err.message); // 'deadline', 'Invalid deadline: ...'
}
}API errors
REST methods throw RainApiError (a subclass of Error) on non-2xx responses,
carrying structured fields so you can branch on code instead of parsing strings:
import { RainApiError } from 'rain-sdk-v2';
try {
await rain.getSomeData(/* ... */);
} catch (err) {
if (err instanceof RainApiError) {
console.error(err.status, err.code, err.endpoint, err.body);
// code: 'NOT_FOUND' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'RATE_LIMITED' | 'SERVER_ERROR' | 'UNKNOWN'
}
}Legitimate empty states are not errors: getPnlByPoolId (no position) and
getTokenPrice (non-whitelisted token) return { statusCode: 200, data: null }
rather than throwing on a 404.
License
MIT
