API Reference
Comprehensive API documentation for CloudBank Prediction Market, covering REST APIs, smart contract interfaces, GraphQL queries, and WebSocket channels.
Authentication
User Authentication (EIP-191 Signature)
1. POST /api/v1/auth/nonce → get challenge nonce
2. sign message with wallet → EIP-191 personal_sign
3. POST /api/v1/auth/login → submit signature and obtain JWT
4. Authorization: Bearer {token} → include in subsequent requestsJWT is signed with HS256. Claims include Subject (wallet address), LoginProvider, and ExpiresAt.
Admin Authentication
- JWT Bearer:
POST /api/v1/admin/auth/loginget access + refresh token - Basic Auth: used during bootstrap phase
Authorization: Basic base64(user:pass) - RBAC:
superadmin(full permissions) /admin(limited permissions)
REST API
Base URL: https://{host}/api/v1
Quick cURL Examples (Auth / Markets / Trades / Wallet)
export HOST="https://docs-test.cloudbank.to"
export API="$HOST/api/v1"
export ADDRESS="0xYourWalletAddress"1) Auth: nonce + login
curl -sS -X POST "$API/auth/nonce" \
-H "Content-Type: application/json" \
-d "{\"address\":\"$ADDRESS\"}"{
"nonce": "7fa3f1d9",
"message": "CloudBank login nonce: 7fa3f1d9",
"expiresAt": "2026-03-06T00:00:00Z"
}After signing, submit login ($SIGNATURE and $MESSAGE must be replaced with actual wallet signature data):
export MESSAGE="CloudBank login nonce: 7fa3f1d9"
export SIGNATURE="0xYourEIP191Signature"
curl -sS -X POST "$API/auth/login" \
-H "Content-Type: application/json" \
-d "{\"address\":\"$ADDRESS\",\"message\":\"$MESSAGE\",\"signature\":\"$SIGNATURE\"}"{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"custodialWalletAddress": "0x4D7E...A9f2",
"expiresAt": "2026-03-06T12:00:00Z"
}export TOKEN="eyJhbGciOi..."2) Markets: query market list
curl -sS "$API/markets?state=TRADING&limit=20&offset=0" \
-H "Authorization: Bearer $TOKEN"{
"items": [
{
"id": "0x2E79B7190c463CD11793520d23F63A3d035A94c2",
"question": "Will BTC close above $80k on Friday?",
"state": "TRADING",
"closeTime": "2026-03-08T12:00:00Z"
}
],
"total": 1,
"limit": 20,
"offset": 0
}3) Trades: submit order
curl -sS -X POST "$API/orderbook/orders" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"marketId":"0x2E79B7190c463CD11793520d23F63A3d035A94c2",
"outcome":"YES",
"side":"BUY",
"orderType":"LIMIT",
"price":"0.62",
"amount":"100"
}'{
"orderHash": "0x8f7b...c31d",
"status": "open",
"filledAmount": "0",
"createdAt": "2026-03-05T10:00:00Z"
}4) Wallet: get info + withdraw
curl -sS "$API/wallet/info" \
-H "Authorization: Bearer $TOKEN"{
"walletAddress": "0x4D7E...A9f2",
"balances": {
"BNB": "0.132",
"USDC": "1250.50"
},
"chainId": 97
}curl -sS -X POST "$API/wallet/withdraw" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"asset":"USDC",
"toAddress":"0xReceiverAddress",
"amount":"25.0"
}'{
"txHash": "0x4b17...9f25",
"asset": "USDC",
"amount": "25.0",
"status": "pending"
}Auth
| Method | Path | Auth | Rate Limit | Description |
|---|---|---|---|---|
| POST | /auth/nonce | - | 100/min/IP | Generate EIP-191 challenge (address -> nonce + message) |
| POST | /auth/login | - | 5/min/IP | Verify signature and return JWT + custodialWalletAddress |
POST /auth/nonce
// Request
{ "address": "0x..." }
// Response
{ "nonce": "abc123", "message": "Sign this message...", "expiresAt": "2026-01-01T00:00:00Z" }POST /auth/login
// Request
{ "address": "0x...", "signature": "0x...", "message": "Sign this message..." }
// Response
{ "token": "eyJ...", "custodialWalletAddress": "0x...", "expiresAt": "2026-01-01T00:00:00Z" }Wallet
| Method | Path | Auth | Rate Limit | Description |
|---|---|---|---|---|
| GET | /wallet/info | JWT | 100/min | Get custodial wallet info |
| DELETE | /wallet/me | JWT | 100/min | Delete custodial wallet (requires non-functional state) |
| POST | /wallet/sign | JWT | 20/min | Sign transaction on behalf (whitelisted contracts) |
| POST | /wallet/withdraw | JWT | 3/min | Withdraw BNB/USDC (daily limit) |
POST /wallet/sign
// Request
{ "to": "0x...", "data": "0x...", "value": "0", "product": "predict" }
// Response
{ "txHash": "0x...", "gasMode": "paymaster", "status": "pending", "createdAt": "..." }POST /wallet/withdraw
// Request
{ "asset": "USDC", "toAddress": "0x...", "amount": "100.5" }
// Response
{ "txHash": "0x...", "asset": "USDC", "amount": "100.5", "fee": "0", "gasMode": "user", "status": "pending" }Orderbook
| Method | Path | Auth | Rate Limit | Description |
|---|---|---|---|---|
| GET | /orderbook/book | - | 120/min/IP | Get orderbook depth |
| POST | /orderbook/orders | JWT | 30/min | Submit order |
| POST | /orderbook/route | JWT | 30/min | Hybrid route (AMM + Orderbook) |
| DELETE | /orderbook/orders/:orderHash | JWT | 30/min | Cancel order |
| GET | /orderbook/heartbeat/status | JWT | 30/min | Get heartbeat status |
| GET | /orderbook/heartbeat/ws | JWT | - | WebSocket heartbeat channel |
GET /orderbook/book
?marketId={id}&outcomeTokenId={tokenId}&depth=20// Response
{ "market": "0x...", "bids": [{"price": "0.65", "size": "100"}], "asks": [...] }Invite Relations
| Method | Path | Auth | Rate Limit | Description |
|---|---|---|---|---|
| GET | /invite-relations/by-inviter | - | 120/min/IP | Query relations by inviter |
| POST | /invite-relations/bind | JWT | 60/min | Bind invite relation |
| GET | /invite-relations/me | JWT | 60/min | Get own invite relations |
Admin — Auth
| Method | Path | Auth | Rate Limit | Description |
|---|---|---|---|---|
| POST | /admin/auth/login | - | 30/min/IP | Admin login |
| POST | /admin/auth/refresh | - | 30/min/IP | Refresh token |
Admin — Users (superadmin)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/users | Admin+SA | List all admins |
| GET | /admin/users/:id | Admin+SA | Get admin details |
| POST | /admin/users | Admin+SA | Create admin |
| PATCH | /admin/users/:id | Admin+SA | Update admin |
| POST | /admin/users/:id/reset-password | Admin+SA | Reset password |
| POST | /admin/users/:id/bind-operation-wallet | Admin+SA | Bind operation wallet |
| POST | /admin/users/:id/unbind-operation-wallet | Admin+SA | Unbind operation wallet |
Admin — Custodial Users
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/custodial-users | Admin | Paginated query for custodial users |
| GET | /admin/custodial-users/:id | Admin | Get user details + wallet |
| DELETE | /admin/custodial-users/:id/wallet | SA | Permanently delete user wallet |
Admin — Operation Wallets (superadmin)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/operation-wallets | SA | List operation wallets |
| GET | /admin/operation-wallets/:id | SA | Get wallet details |
| POST | /admin/operation-wallets | SA | Create operation wallet |
| PATCH | /admin/operation-wallets/:id | SA | Update operation wallet |
| POST | /admin/wallet/sign | Admin | Operation wallet sign-on-behalf transaction |
Admin — Whitelist
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /admin/whitelist | Admin | Add contract whitelist |
| GET | /admin/whitelist | Admin | Query whitelist (filterable by product) |
| PATCH | /admin/whitelist/:id | Admin | Update whitelist entry |
| DELETE | /admin/whitelist/:id | Admin | Delete whitelist entry |
Admin — Invite Relations (superadmin)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin/invite-relations | SA | Paginated query (CSV export supported) |
| POST | /admin/invite-relations/import | SA | Bulk import (<=500 rows per batch) |
System
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health | - | Health check (SQL/Redis/BSC status) |
| GET | /metrics | - | Prometheus metrics |
Error Codes
All error responses follow this format:
{ "code": "error_code", "message": "Human-readable message", "details": {} }| Code | HTTP Status | Description |
|---|---|---|
invalid_request | 400 | Invalid request parameters |
invalid_signature | 401 | EIP-191 signature verification failed |
invalid_nonce | 401 | Nonce is invalid or expired |
unauthorized | 401 | Not authenticated or token expired |
forbidden | 403 | Forbidden |
contract_not_whitelisted | 403 | Contract is not whitelisted |
insufficient_bnb | 400 | Insufficient BNB balance |
insufficient_balance | 400 | Insufficient balance |
daily_limit_exceeded | 429 | Exceeded daily withdrawal limit |
not_found | 404 | Resource not found |
rate_limited | 429 | Rate limit exceeded |
invite_binding_conflict | 409 | Invite relation conflict |
invite_request_replayed | 409 | Duplicate bind request |
invite_self_not_allowed | 400 | Self-invite is not allowed |
internal_error | 500 | Internal server error |
service_unavailable | 503 | Service unavailable |
WebSocket
Orderbook Heartbeat
URL: wss://{host}/api/v1/orderbook/heartbeat/wsAuth: Bearer JWT (query param or header)
Server → Client
{ "type": "heartbeat_connected", "status": { ... } }
{ "type": "heartbeat_ack", "status": { ... } }
{ "type": "error", "message": "Error description" }Client → Server
- Any message can trigger heartbeat keep-alive
- Supports standard WebSocket ping/pong
Smart Contract Interfaces
OptimisticController
Binary prediction market controller that manages lifecycle states: TRADING -> PROPOSED -> DISPUTED -> RESOLVED / CANCELLED.
// Read methods
function getQuestion() external view returns (string memory);
function getMetadata() external view returns (string memory);
function getState() external view returns (MarketState);
function getConditionId() external view returns (bytes32);
function getPositionIds() external view returns (uint256[] memory);
function isActive() external view returns (bool);
function canSettle() external view returns (bool);
function getFeatureFlags() external pure returns (uint256);
// Market management (onlyOwner)
function setMetadata(string calldata metadata) external;
function setOracleAskAfter(uint256 newAskAfter) external;
function depositStake() external;
function proposeOutcome(uint8 outcome, bytes calldata data) external;
function settle(bytes calldata data) external;
function cancelAsInvalid() external;
// Open operations
function disputeOutcome(bytes calldata data) external;
function requestOracleQuestion(bytes calldata data) external payable;
function refundOracleQuestion() external;
function proposeOutcomeFromOracle(bytes calldata data) external;Events:
OutcomeProposed(uint8 indexed outcome, address indexed proposer, uint256 timestamp, bytes32 dataHash)OutcomeDisputed(address indexed disputer, uint256 timestamp, bytes32 dataHash)OracleQuestionRequested(bytes32 indexed oracleQuestionId, uint256 deadline, address indexed requester, uint256 value, bytes32 dataHash)StakeProcessed(address indexed creator, address indexed token, uint256 amount, bool refunded)StateTransition(MarketState indexed from, MarketState indexed to)MarketResolved(uint8 indexed outcome, uint256 timestamp)
BinaryCPMM
Constant-product market maker (x*y=k), 0% fee, based on Gnosis CTF ERC-1155 position tokens.
// Read methods
function getReserves() external view returns (uint256 yesReserves, uint256 noReserves);
function getPrice(uint256 tokenId) external view returns (uint256 price1e18);
function quote(uint256 tokenIn, uint256 amountIn) public view returns (uint256 amountOut);
// Liquidity
function addLiquidity(uint256 collateralAmount) external returns (uint256 lpOut);
function removeLiquidity(uint256 lpIn) external returns (uint256 yesOut, uint256 noOut);
// Trading
function swap(uint256 tokenIn, uint256 amountIn, uint256 minOut) external returns (uint256 amountOut);
function swapCollateralForOutcome(uint256 outcomeTokenId, uint256 collateralAmount, uint256 minOutcomeOut) external returns (uint256 outcomeOut);
function sellOutcomeForCollateral(uint256 outcomeTokenId, uint256 outcomeIn, uint256 minCollateralOut) external returns (uint256 collateralOut);
function sellAllOutcomeForCollateral(uint256 outcomeTokenId, uint256 minCollateralOut) external returns (uint256 collateralOut);Events:
LiquidityAdded(address indexed provider, uint256 collateralIn, uint256 lpOut)LiquidityRemoved(address indexed provider, uint256 lpIn, uint256 yesOut, uint256 noOut)Swapped(address indexed trader, uint256 indexed tokenIn, uint256 amountIn, uint256 indexed tokenOut, uint256 amountOut)CollateralSwappedForOutcome(address indexed trader, uint256 collateralIn, uint256 indexed outcomeTokenId, uint256 outcomeOut)OutcomeSoldForCollateral(address indexed trader, uint256 indexed outcomeTokenId, uint256 outcomeIn, uint256 collateralOut)
DCPPFactory
Market factory that deploys market + AMM via EIP-1167 minimal proxies.
// Create market
function createOptimisticMarket(string calldata question, uint256 livenessPeriod, address, uint256, uint8 outcomeSlots) external returns (address market);
function createOptimisticMarket_V2(string calldata question, uint256 livenessPeriod, address, uint256, uint8 outcomeSlots) external returns (address market);
function createOptimisticMarketWithInitialLiquidity(...) external returns (address market, address amm);
function createOptimisticMarketWithInitialLiquidity_V2(...) external returns (address market, address amm);
function createOptimisticMarket_V3(string calldata question, string calldata metadata, ...) public returns (address market, address amm);
function createOptimisticMarket_V4(..., bytes32 key) external returns (address market, address amm);
// Read methods
function getMarketCount() external view returns (uint256);
function getMarketByIndex(uint256 idx) external view returns (address);
// Management (onlyOwner)
function setOracleAdapter(IYesNoOracleAdapter newAdapter) external;
function setMultiOutcomeFactory(address newFactory) external;MultiOutcomeMarket
Multi-option market wrapper that composes N independent binary markets (one-vs-rest mode).
function optionCount() external view returns (uint256);
function getOption(uint256 index) external view returns (string label, address market, address amm, uint256 yesTokenId, uint256 noTokenId);
function proposeWinner(uint8 winnerIndex, bytes calldata data) external; // onlyOwner
function settleWinner(uint8 winnerIndex) external; // onlyOwner
// Oracle flow
function requestWinnerFromOracle(uint256 deadline, bytes calldata data) external payable returns (bytes32 qid);
function proposeWinnerFromOracle(bytes calldata data) external;
function settleWinnerFromOracle() external;SoraOracle
Minimal Yes/No oracle, answered by the designated provider.
function askYesNoQuestion(string calldata question, uint256 deadline) external payable returns (uint256 questionId);
function provideAnswer(uint256 questionId, bool boolAnswer, uint8 confidenceScore, string calldata dataSource) external; // onlyOracleProvider
function getQuestionWithAnswer(uint256 questionId) external view returns (string, uint256, string, bool, AnswerStatus, uint256);
function refundUnansweredQuestion(uint256 questionId) external;CloudBankVerifyingPaymaster
ERC-4337 AA Paymaster with signature validation, daily quota, and sender whitelist.
function validatePaymasterUserOp(UserOperation calldata userOp, bytes32, uint256 maxCost) external returns (bytes memory context, uint256 validationData);
function postOp(PostOpMode, bytes calldata context, uint256 actualGasCost) external;
function getHash(UserOperation calldata userOp, uint48 validUntil, uint48 validAfter) public view returns (bytes32);
// Management (onlyOwner)
function setVerifyingSigner(address newSigner) external;
function setEnforceSenderAllowlist(bool enabled) external;
function setDailySponsorLimit(uint256 newLimit) external;
function setSenderAllowlist(address sender, bool allowed) external;
function batchSetSenderAllowlist(address[] calldata senders, bool allowed) external;
function deposit() external payable;FactoryRegistry
Factory registry that provides a stable pointer to the current factory address.
function factory() external view returns (address);
function setFactory(address newFactory) external; // onlyOwnerKey Interfaces
// IMarket
interface IMarket {
function getQuestion() external view returns (string memory);
function getState() external view returns (MarketState);
function getConditionId() external view returns (bytes32);
function isActive() external view returns (bool);
function canSettle() external view returns (bool);
function settle(bytes calldata data) external;
}
// IYesNoOracleAdapter
interface IYesNoOracleAdapter {
function ask(string calldata question, uint256 deadline, bytes calldata data) external payable returns (bytes32);
function getAnswer(bytes32 oracleQuestionId) external view returns (bool finalized, uint8 outcome);
function refund(bytes32 oracleQuestionId) external returns (uint256 refunded);
}
// IConditionalTokens (Gnosis CTF)
interface IConditionalTokens {
function prepareCondition(address oracle, bytes32 questionId, uint256 outcomeSlotCount) external;
function reportPayouts(bytes32 questionId, uint256[] calldata payouts) external;
function splitPosition(IERC20 collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata partition, uint256 amount) external;
function mergePositions(IERC20 collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata partition, uint256 amount) external;
function redeemPositions(IERC20 collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external;
}GraphQL (The Graph / Goldsky)
Endpoint: https://api.goldsky.com/api/public/project_{id}/subgraphs/cloudbank-bsc-testnet/{version}/gn
Schema Entities
Market
type Market @entity {
id: ID! # Market contract address
question: String! # Market question
conditionId: Bytes! # CTF condition ID
state: Int! # 0=TRADING, 1=PROPOSED, 2=RESOLVED
amm: Bytes # AMM contract address
yesTokenId: BigInt # YES token ID (ERC-1155)
noTokenId: BigInt # NO token ID (ERC-1155)
creator: Bytes! # Creator address
createdAtTimestamp: BigInt!
totalVolume: BigDecimal! # Total volume (USDC)
metadataUri: String # IPFS metadata URI
category: String # Market category (parsed from IPFS)
isMultiOutcome: Boolean! # Is multi-outcome market
multiOutcomeMarket: MultiOutcomeMarket
optionIndex: Int # Multi-outcome option index
optionLabel: String # Multi-outcome option label
trades: [Trade!]! @derivedFrom(field: "market")
}Trade
type Trade @entity {
id: ID! # txHash-logIndex
market: Market!
amm: Bytes!
trader: Bytes!
user: User!
type: String! # "buy" | "sell"
side: String! # "yes" | "no"
amountIn: BigInt!
amountOut: BigInt!
price: BigDecimal! # amountIn / amountOut
outcomeTokenId: BigInt!
timestamp: BigInt!
blockNumber: BigInt!
txHash: Bytes!
}User
type User @entity {
id: ID! # Address (lowercase)
totalVolume: BigDecimal!
realizedPnL: BigDecimal!
totalTrades: Int!
totalBuys: Int!
totalSells: Int!
totalProfitableSells: Int!
firstTradeTimestamp: BigInt!
lastTradeTimestamp: BigInt!
trades: [Trade!]! @derivedFrom(field: "user")
positions: [UserPosition!]! @derivedFrom(field: "user")
}UserPosition
type UserPosition @entity {
id: ID! # user-market-side
user: User!
market: Market!
side: String! # "yes" | "no"
shares: BigDecimal! # Current holdings
totalBought: BigDecimal!
totalSold: BigDecimal!
costBasis: BigDecimal! # Cost basis
proceeds: BigDecimal! # Cumulative proceeds
isOpen: Boolean!
openedAtTimestamp: BigInt!
lastTradeTimestamp: BigInt!
}MultiOutcomeMarket
type MultiOutcomeMarket @entity {
id: ID!
question: String!
metadataUri: String
creator: Bytes!
optionCount: Int!
options: [Market!]! @derivedFrom(field: "multiOutcomeMarket")
createdAtTimestamp: BigInt!
}Common Queries
Get all markets
query GetMarkets {
markets(orderBy: createdAtTimestamp, orderDirection: desc) {
id, question, state, amm, yesTokenId, noTokenId, totalVolume, category
trades(first: 1, orderBy: timestamp, orderDirection: desc) { price, side }
}
}Get market trades
query GetMarketTrades($marketId: ID!) {
trades(where: { market: $marketId }, orderBy: timestamp, orderDirection: desc, first: 100) {
id, trader, type, side, amountIn, amountOut, price, timestamp, txHash
}
}Get user positions
query GetUserPositions($user: ID!) {
user(id: $user) {
positions(where: { isOpen: true }) {
side, shares, costBasis, proceeds
market { id, question, state }
}
}
}Leaderboard (by volume)
query GetTopTraders($first: Int!) {
users(orderBy: totalVolume, orderDirection: desc, first: $first, where: { totalTrades_gt: 0 }) {
id, totalVolume, realizedPnL, totalTrades, totalProfitableSells
}
}Get multi-outcome markets
query GetMultiOutcomeMarket($id: ID!) {
multiOutcomeMarket(id: $id) {
id, question, optionCount
options(orderBy: optionIndex) {
id, optionLabel, state, amm, yesTokenId, noTokenId, totalVolume
}
}
}Indexed Events
The Graph subgraph listens to the following on-chain events:
| Contract | Event | Handler | Entity |
|---|---|---|---|
| DCPPFactory | MarketCreatedWithAMM | handleMarketCreatedWithAMM | Market |
| DCPPFactory | MarketCreatedWithAMMWithTokens | handleMarketCreatedWithAMMWithTokens | Market |
| MultiOutcomeMarketFactory | MultiOutcomeMarketCreated | handleMultiOutcomeMarketCreated | MultiOutcomeMarket |
| MultiOutcomeMarketFactory | MultiOutcomeOptionCreated | handleMultiOutcomeOptionCreated | Market |
| BinaryCPMM (template) | CollateralSwappedForOutcome | handleBuy | Trade, User, UserPosition |
| BinaryCPMM (template) | Swapped | handleSell | Trade, User, UserPosition |
| BinaryCPMM (template) | OutcomeSoldForCollateral | handleOutcomeSoldForCollateral | Trade, User, UserPosition |
Contract Addresses (BSC Testnet)
| Contract | Address |
|---|---|
| FactoryRegistry | 0xCC74a39C33a6058a18c4658590cCd3137d8DdC89 |
| DCPPFactory | 0x615e991d7be7d6f40f4cdd797a94c972bce91917 |
| MultiOutcomeMarketFactory | 0xdb714af139fce03d68496473cebd9d9d8b72b990 |
| ConditionalTokens (CTF) | 0x2C9e65D17C01fEC736DcF5857C408483ec0a0D72 |
| Collateral Token (USDC) | 0x71172871BCbe41dD8624Ede0294a56FA0FB87622 |
Rate Limiting
Redis-based token bucket strategy with a sliding 1-minute window, segmented by User ID / Admin ID / IP.
| Endpoint Category | Limit | Identifier |
|---|---|---|
| Auth nonce | 100/min | IP |
| Auth login | 5/min | IP |
| Wallet info | 100/min | User |
| Sign tx | 20/min | User |
| Withdraw | 3/min | User |
| Invite bind | 60/min | User |
| Orderbook read | 120/min | IP |
| Orderbook write | 30/min | User |
| Admin auth | 30/min | IP |
| Admin ops | 120/min | Admin |
| Admin sign | 30/min | Admin |
When exceeded, returns 429 Too Many Requests + Retry-After header.