Non-custodial strategy vaults on the Rain prediction-market protocol. Written for professional traders, market makers, bots, and AI agents — everything needed to create, fund, manage, trade, and exit vaults, fully on-chain.
Rain Vaults lets anyone run a trading strategy on Rain prediction markets with pooled capital, Hyperliquid-style:
| Item | Address |
|---|---|
| Network | Arbitrum One (chainId 42161) |
| VaultFactory (v2, current) | 0x8c373dcfb227e8c00a30718a7daf0270a960f8b7 |
| VaultFactory (v1, legacy — no trading keys) | 0x35ccc0a078af070c5a4203a1af43b7a99995d475 |
| Base token — USD₮0 (USDT, 6 decimals) | 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9 |
| Rain market factory (prod) | 0x38B3Ba1ee001E6785224E31b3031ae96CA06C677 |
| Public RPC | https://arb1.arbitrum.io/rpc |
| Role | Can do | Cannot do |
|---|---|---|
| Manager (vault creator) | Trade on registered markets, register markets, authorize/revoke trading keys, close the vault, withdraw own shares (respecting 5% minimum) | Withdraw depositor funds, change fees (immutable), drop below 5% ownership while others hold shares |
| Trading key / operator (bot, AI agent) | Trade on registered markets, register markets | Withdraw anything, authorize other keys, close the vault |
| Depositor | Deposit, request withdrawal (after lock-up), cancel own queue request, force-unwind after grace, crank fees/queue via poke() | Trade |
| Anyone | claim() resolved winnings for the vault, processQueue(), forceUnwind() when conditions are met | — |
USDT.approve(factory, seedAmount). Minimum seed: 100 USDT (MIN_SEED = 100e6).createVault(name, description, performanceFeeBps, managementFeeBps, lockupPeriod, seedAmount):| Param | Type / limits | Meaning |
|---|---|---|
| name | string, immutable | Vault display name |
| description | string, immutable | Strategy description shown to depositors |
| performanceFeeBps | 0–5000 (= 0–50%) | % of profit above high-water mark, paid to manager |
| managementFeeBps | 0–500 (= 0–5%/yr) | Annual fee on assets, accrued per second |
| lockupPeriod | 0–2592000 sec (0–30 d) | Per-deposit withdrawal lock |
| seedAmount | ≥ 100e6 | Manager's own deposit (skin in the game) |
The factory deploys a new RainVault, credits the seed shares to you, and lists the vault in getAllVaults(). Fees are immutable forever — displayed to every depositor.
USDT.approve(vault, amount);
vault.deposit(amount); // credits msg.sender
// or vault.depositFor(beneficiary, amount);
amount × totalShares / NAV (first deposit: 1 share per USDT-wei, plus 1000 dead shares as an inflation-attack guard).sharePrice = NAV / totalShares (1e18 scale).firstBuyOrderPrice) + full claim value of resolved options. Conservative marking protects existing depositors from overpriced entries.Vault page → Manager Panel (visible when connected as manager): register market, place/cancel orders.
setOperator(address, true).setOperator(address, false).An authorized key calls the vault's trading functions directly — no manual signing, fully automated, 24/7:
// All trading functions — callable by manager OR authorized operators:
registerMarket(address market) // one-time per market; grants USDT approval
tradePlaceBuyOrder(market, option, side, price, amount, postOnly) // limit buy (USDT amount)
tradePlaceSellOrder(market, option, side, price, shares, postOnly) // limit sell (shares)
tradeEnterOption(market, option, side, amount, minSharesOut) // taker/AMM buy
tradeSellOption(market, option, side, shares, minAmountOut) // market-sell into bids (order-book markets)
tradeCancelBuyOrders(market, option, sides[], prices[], orderIDs[])
tradeCancelSellOrders(market, option, sides[], prices[], orderIDs[])
tradeSplit(market, option, amount) // USDT → equal YES+NO pairs
tradeMerge(market, option, amount) // YES+NO pairs → USDT
claimResolved(market, option) // anyone; claims resolved winnings to the vault
GET https://prod2-api.rain.one/pools/public-pools?limit=30 (field contractAddress).490000000000000000. Tick spacing 0.01.100000000.firstBuyOrderPrice(option, side) / firstSellOrderPrice(option, side) on the market contract.tradingModel() → 0 = AMM (buy/split/merge only, no sellOption before resolution), 1 = OrderBook (full limit-order trading).claim after resolution). Prices ≈ implied probability.poke().Three paths, fastest first:
requestWithdraw(shares) — if the vault has enough free USDT and your lock-up passed, you're paid immediately at current NAV.forceUnwind(positionIndex, side, minAmountOut): cancels resting orders / claims resolved options / market-sells up to 20% of a position per call (order-book) or merges YES+NO pairs (AMM) until the queue is paid. Slippage is borne at fill time — those demanding liquidity pay its true market price; remaining depositors are not diluted.LockupActive.ManagerBelowMinimum).cancelWithdrawRequest(index).closeVault() — accrues final fees, freezes trading.withdrawAfterClose(shares) — no lock-up, no queue.deposit(uint256 amount) → uint256 shares
depositFor(address beneficiary, uint256 amount) → uint256 shares
requestWithdraw(uint256 shares) → bool instant
cancelWithdrawRequest(uint256 index)
withdrawAfterClose(uint256 shares)
processQueue(uint256 maxRequests) // permissionless crank
poke() // accrue fees + process queue
forceUnwind(uint256 positionIndex, uint8 side, uint256 minAmountOut) // after 48h grace
forceCancelOrders(uint256 positionIndex, bool buySide, uint8[] sides, uint256[] prices, uint256[] orderIDs)
setOperator(address operator, bool enabled) // authorize/revoke trading keys
closeVault()
See section 6b.
vaultInfo() → (name, manager, nav, totalShares, sharePrice, perfFeeBps, mgmtFeeBps, lockup, closed)
nav() → uint256 // USDT, 6 decimals, conservative mark
sharePrice() → uint256 // 1e18 scale
sharesOf(address) → uint256
lastDepositAt(address) → uint256 // unix; + lockupPeriod() = unlock time
lockupPeriod() → uint256
operators(address) → bool
operatorsCount() / operatorList(uint256)
allowedMarket(address) → bool
marketsCount() / markets(uint256)
trackedPositionsCount() / trackedPositions(uint256) → (market, option)
queueLength() / queueHead() / queue(uint256) → (owner, shares, requestedAt)
highWaterMark() → uint256
createVault(string name, string desc, uint256 perfBps, uint256 mgmtBps, uint256 lockup, uint256 seed) → address
getAllVaults() → address[]
vaultsByManager(address, uint256) → address
MIN_SEED() → uint256 // 100e6
Deposit(user, amount, shares, sharePrice)
WithdrawRequested(user, shares, queueIndex)
Withdrawn(user, shares, amount, sharePrice)
MarketRegistered(market)
PositionTracked(market, option)
ManagerTrade(market, selector, option, side, amount)
PerformanceFee(manager, feeShares, newHighWaterMark)
ManagementFee(manager, feeShares)
ForceUnwind(caller, market, option, recovered)
OperatorSet(operator, enabled)
VaultClosedEvent(finalSharePrice)
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://arb1.arbitrum.io/rpc');
const key = new ethers.Wallet(process.env.TRADING_KEY, provider); // authorized via setOperator
const vault = new ethers.Contract(VAULT_ADDR, VAULT_ABI, key);
const market = new ethers.Contract(MARKET_ADDR, [
'function firstBuyOrderPrice(uint256,uint8) view returns (uint256)',
'function firstSellOrderPrice(uint256,uint8) view returns (uint256)',
], provider);
const bid = await market.firstBuyOrderPrice(1, 1); // option 1, YES — 1e18 scale
const ask = await market.firstSellOrderPrice(1, 1);
await vault.registerMarket(MARKET_ADDR); // once per market
await vault.tradePlaceBuyOrder(
MARKET_ADDR,
1, // option (1-based)
1, // side: YES=1, NO=2
ethers.parseUnits('0.48', 18), // price, 1e18
ethers.parseUnits('50', 6), // USDT amount, 6 decimals
false // postOnly
);
await vault.tradeSellOption(MARKET_ADDR, 1, 1, ethers.parseUnits('100', 6), 0);
const [name, manager, nav, shares, price] = await vault.vaultInfo();
const myPos = await market.userOptionPerSideShares(1, 1, VAULT_ADDR); // vault's YES shares
await vault.claimResolved(MARKET_ADDR, 1);
baseToken() is USDT — the vault cannot approve arbitrary contracts.