Module 17: Contracts & API
Reference module for contract addresses, SDK setup, API details, production operations, and error handling. Organized for quick lookup.
Prerequisites
- Wallet basics and SDK install via Module 02
- For an explanation of which contracts back which actions, see Module 18
Next steps
- Pick an action module to start writing code: trading, lending, staking, token creation, predictions, vesting
- For agent identity, see Module 03
- For full error context, see the SDK Reference error index
1. Contract Addresses
Addresses are published at /contracts.json on the dapp host the SDK talks to (apiDomain). Never copy an address from documentation. /contracts.json is generated from the dapp's deployment config, one network per deployment. The SDK fetches it at start-up and compares it with its built-in defaults. An all-zero address means that contract is not deployed on that network.
Keys of /contracts.json:
| Key | Contract | Purpose |
|---|---|---|
chain, chainId, network, deploymentBlock | - | The network of this deployment |
usdb | the default main's pair asset | Pair asset of the default main (USDG on Robinhood Chain; the key keeps its old name) (→01) |
mainToken | A_STASISTOKEN | The default main (every main is listed in mains[]) |
swap | ASwap | Every buy, sell, leverage buy, partial loan sell and cross swap |
factory | ATokenFactory | Token creation |
staking | AStasisVault | The vault: a 1:1 stake of a main, not a token. One contract serves every main, each with its own pool (mains[].vault names it per main); every call names the main |
loanHub | ALOAN_HUB | Hub loans (a leverage position is recorded on its token's main, not here) |
vesting | A_VestingContract | Vesting schedules and vesting loans |
taxes | ATaxes | Fees, the creator and rewards fee setters, on-chain referrals |
marketTrading (alias tranche) | ATrancheMarket | Public market host and its order book |
privateMarket (alias tranchePrivate) | ATrancheMarketPrivate | Private market host (creator-managed markets) |
resolver | AMarketResolver | Resolution of public markets |
bondToken, bondDecimals | - | The token resolver bonds are posted in, and its decimals |
trancheReader (also reader) | TrancheReader | Market read helper: every call takes the host first (→10) |
redemptionReader | RedemptionReader | Redemption quotes |
leverage | ALEVERAGE | Leverage simulation (views) |
liquidator | ALiquidator | Liquidation of expired loans and positions, keepers only |
hopExecutor | AHopExecutor | The outside-pool hops of a cross swap |
upDown | MultiPrediction | One contract for every Up/Down asset and timeframe (→08) |
erc8004Registry | external | On-chain agent identity; the zero address means agent registration is off on that network |
wrappedNative | external | Wrapped ETH (WETH), for cross swaps |
mains[] | A_STASISTOKEN + AStasisVault per entry | { key, label, name, symbol, main, pairAsset, vault, pairSymbol, pairDecimals }. mainToken, usdb and staking are the default main's main, pairAsset and vault |
Naming note: MAINTOKEN is the SDK's name for the default main:
client.mainTokenAddress(JS) /client.main_token_address(Python). Each main has its own name and symbol on chain; tell mains apart by address or bymains[].symbol.
Constructor override names: factoryAddress, swapAddress, marketTradingAddress, loanHubAddress, vestingAddress, stakingAddress, resolverAddress, privateMarketAddress, readerAddress, leverageAddress, taxesAddress, usdbAddress, mainTokenAddress, liquidatorAddress, hopExecutorAddress, redemptionReaderAddress, upDownAddress, mains. usdbAddress, mainTokenAddress and stakingAddress are the default main's pair asset, the default main and its vault.
Project Vetting (projectedVetted)
Each Factory token contract carries a projectedVetted boolean that marks it as reviewed by BASIS. The flag is exposed in token responses from GET /api/v1/tokens/{address} as projectedVetted: boolean | null:
true: vetted; the dapp renders an "Official" badge next to the token name.false: explicitly not vetted.null, not yet read from chain (lazy-fill on first read; falls back to null if the chain call fails).
SDK consumers should treat the field as read-only and may render their own UI when projectedVetted === true.
2. SDK Installation & Configuration
Installation
JavaScript / TypeScript:
npm install github:Launch-On-Basis/SDK-TS
Python:
pip install git+https://github.com/Launch-On-Basis/SDK-PY.git
Initialization Modes
Three modes unlock progressively more functionality.
Mode 1: Read-Only (no credentials)
On-chain reads only. No private key or API key required.
JavaScript:
const { BasisClient } = require("basis-sdk-js");
const client = new BasisClient();
// price in the pair asset of the token's main: raw pair units per whole token
const price = await client.trading.getPairPrice("0xTokenAddress...");
Python:
from basis_sdk import BasisClient
client = BasisClient()
# price in the pair asset of the token's main: raw pair units per whole token
price = client.trading.get_pair_price("0xTokenAddress...")
Mode 2: API Key (read-only + off-chain data)
Adds access to off-chain endpoints: token lists, candles, trade history, etc.
JavaScript:
const client = new BasisClient({ apiKey: "bsk_your_api_key" });
const tokens = await client.api.getTokens({ limit: 10 });
Python:
client = BasisClient(api_key="bsk_your_api_key")
tokens = client.api.get_tokens(limit=10)
Mode 3: Full Mode (private key, recommended for agents)
Automatically authenticates via SIWE, auto-provisions an API key, and enables all write operations. This is the mode to use for agents.
On first run with no existing API key, the SDK creates a new key and logs it. Save this key: it is only shown once. Pass it on subsequent runs via apiKey.
JavaScript:
// First run: SDK creates and logs a new API key. Save it!
const client = await BasisClient.create({ privateKey: process.env.BASIS_PRIVATE_KEY });
// Subsequent runs: pass the saved key
const client = await BasisClient.create({
privateKey: process.env.BASIS_PRIVATE_KEY,
apiKey: "bsk_your_saved_key",
});
// Execute a trade: the amount is in the pair asset of the token's main
const { parseUnits } = require("viem");
const pairDecimals = 6; // that pair asset's decimals: /contracts.json mains[].pairDecimals (6 for USDG)
const result = await client.trading.buy("0xTokenAddress...", parseUnits("5", pairDecimals));
console.log("Tx hash:", result.hash);
Python:
import os
# First run
client = BasisClient.create(private_key=os.environ["BASIS_PRIVATE_KEY"])
# Subsequent runs
client = BasisClient.create(
private_key=os.environ["BASIS_PRIVATE_KEY"],
api_key="bsk_your_saved_key"
)
pair_decimals = 6 # the pair asset's decimals: /contracts.json mains[].pairDecimals (6 for USDG)
result = client.trading.buy("0xTokenAddress...", 5 * 10**pair_decimals)
print("Tx hash:", result["hash"])
Session lifetime: SIWE sessions expire when the browser closes (no TTL). For long-running agents, API keys are better: they are auto-provisioned during
BasisClient.create()and do not expire.client.apiKeypersists across restarts if you store it.
Configuration Options
All options are passed to BasisClient constructor or BasisClient.create.
| Option | Type | Default | Description |
|---|---|---|---|
privateKey | string | - | Wallet private key. Enables write operations and automatic SIWE auth. |
apiKey | string | - | API key for data endpoints. On first run with privateKey, a key is auto-created and logged: save it and pass it on future runs. Only shown once at creation. |
rpcUrl | string | the SDK's preset for the deployment's chain | RPC endpoint. Validated on connect: it must return the chainId published at /contracts.json. |
apiDomain | string | https://launchonbasis.com | Base URL for the Basis API. |
agent | boolean or object | - | ERC-8004 agent registration. true for defaults, or { name, description, capabilities } for custom metadata. Register after building real capabilities, not on day one. |
Client Properties
JavaScript (camelCase):
| Property | Type | Description |
|---|---|---|
client.usdbAddress | address | The default main's pair asset (USDG on Robinhood Chain) |
client.mainTokenAddress | address | The default main |
client.stakingAddress | address | The vault, one contract for every main (a stake, not a token) |
client.addresses | object | Every key of /contracts.json, mains[] included |
client.mains() | Promise<MainInfo[]> | Every main: SWAP.pairCount(), pairList(i), pairs(main) → (pairAsset, staking, active) |
client.publicClient | PublicClient | viem public client for read-only calls |
client.walletClient | WalletClient | viem wallet client for write operations (requires privateKey) |
client.walletClient.account.address | address | Your wallet address |
client.api | BasisAPI | Off-chain API wrapper |
client.apiKey | string | Auto-provisioned API key (persistent, no expiry) |
Python (snake_case):
| Property | Type | Description |
|---|---|---|
client.w3 | Web3 | web3.py instance for raw contract calls |
client.wallet_address | str | Your wallet address |
client.usdb_address | str | The default main's pair asset (USDG on Robinhood Chain) |
client.main_token_address | str | The default main |
client.api_key | str | Auto-provisioned API key (persistent, no expiry) |
Token Amount Conventions
All SDK methods expect raw integer amounts in the token's smallest unit. Tokens minted by BASIS (every main, factory token, market token and outcome share) have 18 decimals. A main's pair asset and the resolver's bond token carry their own decimals: read PAIR_UNIT() on the main (or decimals() on the asset) and BOND_UNIT() on the resolver. Never assume 18 for them.
| Token | Decimals | Example |
|---|---|---|
| USDG (the default main's pair asset) | 6 | 5 * 10**6 = 5 USDG |
| Any other pair asset | decimals() of the asset | parseUnits("5", decimals) |
| Bond token | BOND_UNIT() | - |
| Mains | 18 | 1 * 10**18 = 1 main token |
| Factory tokens | 18 | 1 * 10**18 = 1 token |
JavaScript:
import { parseUnits, formatUnits } from "viem";
const decimals = 18; // the pair asset's decimals (PAIR_UNIT() on the main, or decimals() on the asset)
const pairRaw = parseUnits("5", decimals); // 5000000000000000000n for an 18-decimal asset
const human = formatUnits(pairRaw, decimals); // "5"
Python:
decimals = 18 # the pair asset's decimals
pair_raw = 5 * 10**decimals # 5000000000000000000 for an 18-decimal asset
human = pair_raw / 10**decimals # 5.0
Exception: the partial loan sell takes a percentage, 10 to 100 in steps of 10, not a raw amount.
Private Key Security
Never hardcode private keys in source files or commit them to version control.
// JS: use environment variables
const client = await BasisClient.create({ privateKey: process.env.BASIS_PRIVATE_KEY });
# Python: use environment variables
import os
client = BasisClient.create(private_key=os.environ["BASIS_PRIVATE_KEY"])
Best practices:
- Store keys in
.envfiles (add.envto.gitignore) - Use a secrets manager for production (AWS Secrets Manager, HashiCorp Vault, etc.)
- Generate a dedicated wallet for your agent: do not reuse personal wallets
- Keep ETH in the wallet for gas
3. API Reference
Base URL
https://launchonbasis.com/api
Rate Limits
| Auth Type | Limit | Scope |
|---|---|---|
API Key (/api/v1/*) | 60 req/min | Per key |
| SIWE Session (core endpoints) | 30 req/min | Per IP |
Transaction Sync (/api/v1/sync) | 60 req/min | Per IP (the platform's backfill worker has its own key and limit) |
When exceeded: 429 Too Many Requests
Rate limit headers on every response:
X-RateLimit-Limit: max requests per windowX-RateLimit-Remaining: requests left in current windowX-RateLimit-Reset: unix timestamp when the window resets
Pagination
Offset-based (browsable lists: tokens, orders, comments, whitelist):
?page=1&limit=20
→ { "total": 100, "page": 1, "limit": 20, "hasMore": true }
Cursor-based (append-only data: trades, transactions, liquidity):
?limit=20 // first page
?cursor=499&limit=20 // next page (use nextCursor from previous response)
→ { "limit": 20, "hasMore": true, "nextCursor": "479" }
Each endpoint notes which style it uses.
Authentication
| Method | How | Lifetime |
|---|---|---|
| SIWE session | client.authenticate() | Until browser/process closes |
| API key | Pass apiKey to constructor | No expiry |
For long-running agents: use API keys. BasisClient.create() auto-provisions a key: store it and pass it on restarts.
HTTP Status Codes
| Status | Meaning |
|---|---|
400 | Bad request (missing or invalid parameters) |
401 | Not authenticated (missing or expired session/API key) |
403 | Forbidden (not the owner or insufficient permissions) |
404 | Resource not found |
409 | Conflict (duplicate resource, e.g. metadata already exists) |
422 | Validation failed (invalid signature, sync error) |
429 | Rate limit exceeded |
Transaction Sync
The SDK automatically syncs transaction state to the backend after every write operation. This calls POST /api/v1/sync with the transaction hash: no auth required, idempotent.
If the sync fails, a warning is logged but the on-chain transaction is unaffected. Manual sync if needed:
await client.api.syncTransaction(txHash);
client.api.sync_transaction(tx_hash)
The legacy
syncLoan/sync_loanstill works but is deprecated: it delegates tosyncTransaction.
4. Production Operations
Agent Lifecycle
1. INIT → Create client, register identity, fund the pair asset (USDG) and ETH for gas
2. BUILD → Develop and test strategies
3. REGISTER → Publish capabilities to ERC-8004 (after building real functionality)
4. OPERATE → Run strategies, manage positions, collect fees and rewards
5. MONITOR → Watch positions, check health, handle alerts
6. RECOVER → Rebuild state after crashes, handle RPC/network failures
7. SHUTDOWN → Close positions, repay loans, unstake, withdraw
Health Checks
Run every 1–5 minutes for active agents:
async function healthCheck(client) {
const wallet = client.walletClient.account.address;
// 1. RPC connectivity
try {
const blockNumber = await client.publicClient.getBlockNumber();
} catch (e) {
console.error("RPC DOWN:", e.message);
return false; // Switch to backup RPC or alert
}
// 2. Pair-asset balance (repeat for each main you use; resolvers also check BOND_TOKEN)
const pairBalance = await client.publicClient.readContract({
address: client.usdbAddress,
abi: [{"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"}],
functionName: 'balanceOf',
args: [wallet],
});
// 3. ETH balance for gas (MIN_GAS_BALANCE: a threshold you set)
const nativeBalance = await client.publicClient.getBalance({ address: wallet });
if (nativeBalance < MIN_GAS_BALANCE) {
console.warn("Low ETH - refill for gas");
}
// 4. Loan expiry
const loanCount = await client.loans.getUserLoanCount(wallet);
for (let i = 1n; i <= loanCount; i++) {
const loan = await client.loans.getUserLoanDetails(wallet, i);
if (loan.active) {
const hoursLeft = (Number(loan.liquidationTime) * 1000 - Date.now()) / 3_600_000;
if (hoursLeft < 24) console.warn(`Loan ${i} expires in ${hoursLeft.toFixed(1)}h`);
}
}
return true;
}
Error Recovery Patterns
RPC Timeout / 429
async function withRetry(fn, maxRetries = 3, baseDelayMs = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (e) {
const isRetryable = e.message?.includes('timeout') ||
e.message?.includes('429') ||
e.message?.includes('ECONNRESET');
if (!isRetryable || attempt === maxRetries) throw e;
const delay = baseDelayMs * Math.pow(2, attempt - 1);
await new Promise(r => setTimeout(r, delay));
}
}
}
const result = await withRetry(() => client.trading.buy(tokenAddr, amount));
Stuck Transaction
If a transaction is pending too long:
- Check the receipt first: if it exists, the transaction went through
- If still pending after 60s, resubmit with higher gas
- Never assume a timed-out transaction failed: always check the receipt before retrying
async function waitForTxSafe(client, hash, timeoutMs = 60000) {
try {
return await client.publicClient.waitForTransactionReceipt({ hash, timeout: timeoutMs });
} catch (e) {
// Timeout: check if it landed anyway
try {
const receipt = await client.publicClient.getTransactionReceipt({ hash });
if (receipt) return receipt;
} catch {}
throw new Error(`Transaction ${hash} timed out and may still be pending`);
}
}
Chain Reorg
Block time and reorg depth depend on the deployment's chain:
- Wait for 3+ confirmations before treating a transaction as final (especially for market finalization, loan extensions near expiry)
- Use
waitForTransactionReceipt({ hash, confirmations: 3 })for critical operations - Do not chain time-dependent transactions in rapid succession
SIWE Session Expired (401)
For long-running agents, use API keys: they do not expire. If you hit a 401:
// Re-create client to get a fresh API key
const client = await BasisClient.create({ privateKey: process.env.BASIS_PRIVATE_KEY });
// client.apiKey is now refreshed
State Reconstruction After Crash
All position data lives on-chain. The blockchain is the source of truth. If the API is down, all positions can be read directly via RPC.
async function reconstructState(client) {
const wallet = client.walletClient.account.address;
const state = { loans: [], leveragePositions: [], staking: {} };
// 1. Active loans
const loanCount = await client.loans.getUserLoanCount(wallet);
for (let i = 1n; i <= loanCount; i++) {
const loan = await client.loans.getUserLoanDetails(wallet, i);
if (loan.active) state.loans.push({ hubId: i, ...loan });
}
for (const m of await client.mains()) {
// 2. Leverage positions: recorded per main (leverageCount(user) / leverages(user, id) on each main in SWAP.pairList)
const levCount = await client.trading.getLeverageCount(wallet, m.main);
for (let i = 1n; i <= levCount; i++) {
const pos = await client.trading.getLeveragePosition(wallet, i, m.main);
if (pos.active) state.leveragePositions.push({ main: m.main, positionId: i, ...pos });
}
// 3. Vault position on this main (positions(main, user) + earned(main, user) on the vault; a stake, not a token)
// { staked, pledged, hubId, checkpoint, pending, hub } plus earned, collateralOf, freeToUnstake; a loan is open when hub is set
state.staking[m.main] = await client.vault(m.main).getPosition(wallet);
}
// 4. Vesting (creator and beneficiary)
const vestingIds = await client.vesting.getVestingsByCreator(wallet);
const beneficiaryIds = await client.vesting.getVestingsByBeneficiary(wallet);
console.log(`Reconstructed: ${state.loans.length} loans, ${state.leveragePositions.length} leverage positions`);
return state;
}
RPC Configuration
A public RPC endpoint has no SLA. Robinhood Chain's public RPC rate-limits after a few dozen calls. For production agents:
Use a dedicated provider that supports Robinhood Chain. Check the provider's chain list before you pay. Free tiers can be tight: Alchemy's free tier, for one, allows only 10-block eth_getLogs ranges.
Failover pattern:
const RPC_ENDPOINTS = [
"https://your-primary-rpc.com",
"https://your-backup-rpc.example",
];
async function createClientWithFailover() {
for (const rpc of RPC_ENDPOINTS) {
try {
const client = await BasisClient.create({
privateKey: process.env.BASIS_PRIVATE_KEY,
rpcUrl: rpc,
});
return client;
} catch (e) {
console.warn(`RPC ${rpc} failed:`, e.message);
}
}
throw new Error("All RPC endpoints failed");
}
Transaction Sequencing
Always await the receipt before sending the next transaction. The SDK uses viem which manages nonces for sequential calls.
// Correct: sequential, each buy() internally awaits receipt
const tokens = ["0xToken1", "0xToken2", "0xToken3"];
for (const token of tokens) {
const result = await client.trading.buy(token, parseUnits("10", pairDecimals)); // pair asset of the token's main
}
// Wrong: parallel sends cause nonce collisions
// await Promise.all(tokens.map(t => client.trading.buy(t, amount)));
Monitoring Checklist
| What | Method | Alert When |
|---|---|---|
| Loan expiry | getUserLoanDetails(wallet, hubId) → liquidationTime | < 24 hours remaining |
| Leverage expiry (per main) | getLeveragePosition() → liquidationTime | < 24 hours remaining |
| Vault loan expiry (per main) | positions(main, wallet) on the vault → hubId, then that hub loan's liquidationTime | < 24 hours remaining |
| Expired, not yet liquidated | liquidationTime < now and active | Repay or extend now |
| Liquidated by a keeper | the loan's isLiquidated | true: claim the remainder within 365 days of the liquidation; after that the leftover may no longer be claimable |
| ETH gas balance | getBalance() | Below your gas budget |
| Pair-asset balance, per main | balanceOf() on the pair asset | Below your minimum threshold |
| Token fee | getTaxRate(token, wallet) | Above the rate you planned for |
| Prediction market status | on chain: getMarketData(market) (end time), hasProposal(market), inDispute(market), inVeto(market) | A market you watch needs a proposal or is in dispute |
| Vault rewards | earned(main, wallet) on the vault, per main | Worth claiming or compounding |
| Bond token balance (resolvers) | balanceOf() on BOND_TOKEN() | Below requiredBond(market, wallet) |
| RPC health | getBlockNumber() | Timeout or stale block |
Shutdown Procedure
- Stop opening new positions (halt trading loops)
- Repay loans before expiry: hub loans with
LOANS.repayLoan(hubId, amount)(2^256 - 1repays in full), a vault loan withrepay(main)on the vault. AfterliquidationTimea keeper liquidates the position. Assume it can happen in the first block after the deadline. Until that transaction lands a repay or a paid extension still goes through; do not plan on the gap. - Close leverage positions with
client.trading.partialLoanSellFor(main, positionId, 100n, true, minOut)(contractSWAP.partialLoanSellFor), per position and per main, or repay them: each position is recorded on its token's main, so callrepayLoan(id, 2^256 - 1, true)there, or repay through SWAP withrepayWith(main, id, pairAsset, amount, minPairOut, true, deadline). A hub loan is sold down withLOANS.hubPartialLoanSell(hubId, 100n, minOut), or repaid. Never pass a leverage id tohubPartialLoanSell: it reads the number as a hub id. With no hub loan of that number it revertsInvalid loan; with one it sells that hub loan instead. No view returns what a full close pays: buildminOutfromMainLens.getPartialLoanSellAmounts(main, ...)and a taxed sell quote, and leave room for the exit fee. - Unstake: repay the vault loan (
repay(main)),claim(main)the rewards, thenunstake(main, amount, toMainPair, minMainPair); it is capped byfreeToUnstake(main, you). There is no unlock step and no vote lock. - Claim what is pending:
LOANS.claimLiquidation(hubId)for each hub loan a keeper has liquidated;claimLiquidation(id, true)on the token's main for a liquidated leverage position (claim within 365 days of the liquidation; after that the leftover may no longer be claimable);redeem(market, 10000, 1, minOut)for resolved markets; the Up/Downclaim(asset, tf, roundId, fractionBps, slices, minOut)for settled rounds;claimTokens(vestingId)for vestings. - Verify final state: run
reconstructState()to confirm no orphaned positions
5. Cross-Cutting Errors
Gas Errors
Deadline
- When:
swapCross,repayWith, the hub'srepayLoanWith, marketbuyand Up/Down bets take a deadline;buyTokensandsellTokenstake none - Why: The transaction landed after its deadline
- Fix: Retry with a fresh deadline:
Math.floor(Date.now() / 1000) + 300 - Prevention: Set deadline 5 minutes out, increase gas for faster confirmation
Approval Errors
Approve / Approve failed / approval failed
- These strings are the contracts' own internal approvals. A missing allowance of yours shows as the token's own error (
ERC20: insufficient allowance). - Fix: approve the right spender:
| Action | Token pulled | Spender |
|---|---|---|
| Buy, leverage buy, cross swap | path[0] (the pair asset, or the main) | SWAP |
| Sell | the token sold | SWAP |
| Hub loan: take, add collateral | the collateral token | the loan hub |
| Hub loan: repay, extend paying in the pair asset | the main's pair asset | the loan hub |
Leverage position: repay through SWAP (repayWith) | the token you pay with (the pair asset or another) | SWAP |
| Leverage position: repay or paid extension by contract call | the pair asset | the address you call (the SDK sets the approval) |
| Vault: stake | the main | the vault |
| Vault: repay, extend paying in the pair asset | the pair asset | the vault |
| Vesting: create, add tokens | the vested token | VESTING |
| Vesting loan: repay | the main's pair asset | VESTING |
| Market: seed, order fill | the market's pair asset | the market's host |
| Market: bet | the chosen input token (pair asset, main, market token or another BASIS token) | the market's host |
| Resolver: propose, dispute, veto | BOND_TOKEN() | the resolver |
| Up/Down bet | the chosen input token | the Up/Down contract |
No approval is needed for setReferrer, partial loan sells, redeem, claims or unstake. The liquidator, the hop executor and TAXES are never a spender for a user. Nothing is ever approved on the vault as a token: the vault is not an ERC-20.
Three calls are payable, with their own rules:
- Token creation:
msg.valuemust equalfeeAmount()(0 for a fee-whitelisted caller) exactly, elseWrong fee. - Market creation:
msg.valuemust equal the factory'sfeeAmount()(0 when the host is fee-whitelisted) exactly; a wrong value reverts with no message. - Vesting creation: charged only while
feeEnabledis on and the caller is not whitelisted; needsmsg.value >= feeAmount(Insufficient fee) and refunds the surplus.
Transfer failed (various messages)
- When: Any operation moving tokens
- Why: Insufficient balance or missing approval
- Fix: Verify
balanceOf() >= amountand approval is sufficient
Reentrancy Guard
ReentrancyGuard: reentrant call
- When: Any contract call
- Why: A callback or hook is triggering another call to the same contract
- Fix: Don't make nested calls to the same contract. Use the SDK: it handles call sequencing correctly.
Trading / Slippage
Min out (swaps) / Min out not met (partial loan sell) / Below minOut (redeem); a market bet under minShares reverts with no reason string
- Fix: Build
minOutfrom the taxed quote:getAmountsOutexcludes the fee. Withrate = getTaxRate(token, wallet), a buy quotesgetAmountsOut(amountIn × (10000 − rate) / 10000, path)and a sellgetAmountsOut(amount, path) × (10000 − rate) / 10000(Module 04). Then apply your slippage tolerance. ForswapCross, quote withquoteCross(path, amountIn, trader). If still failing, reduce the amount.
Insufficient balance
- Fix: Check
balanceOf()before transacting
No WL for freeze / Overcapped on WL buying
- When: A presale buy of a frozen token by a wallet that is not whitelisted, or above its cap (a number of tokens)
- Fix: Check
whitelisted(yourAddress)before buying a frozen token.
Presale over / has not bonded yet
- When: They come from the token's own presale and AMM entry points, which only SWAP may call
- Fix: Use
buy()/sell()(SWAP.buyTokens/sellTokens): SWAP picks the presale or the AMM bypresaleComplete(). You never pick a path.
No reason string
- Many checks revert with empty data, most of them in the market contracts. Check the state before sending: paused, resolved, whitelist, invite list, minimum bet, seed floor and step, end time.
API Errors
| Status | Meaning | Fix |
|---|---|---|
401 | Not authenticated | Re-create client with BasisClient.create() to refresh API key |
403 | Forbidden / not the owner | Verify wallet is correct (creator vs beneficiary) |
404 | Resource not found | Verify IDs and addresses |
409 | Conflict / duplicate | Resource already exists: check before creating |
429 | Rate limit exceeded | Back off with exponential retry, respect X-RateLimit-Reset |
Non-Fatal Warnings
Order sync failures after orderBook write operations are logged as warnings but do not throw. The on-chain transaction succeeds regardless. Manually re-sync with client.api.syncTransaction(txHash) if needed.
Loan Errors (Quick Reference)
| Error | Fix |
|---|---|
incorrect loan duration | Days must be between minDaysLoan() and maxDaysLoan() of the main (10 and 1000 today); an extension adds at least minDaysLoan() |
Duration too short | Remaining time < 10 days: extend first, then add collateral |
Minimum loan amount not met | The collateral's value must reach the main's minimumLoan(), in pair-asset units. Preview it with client.leverageSimulator.getCollateralValue (a main) or getCollateralValueHybrid (a factory token) |
Loan not active | Loan was liquidated or repaid: check loan.active first |
Loan is still active | claimLiquidation before a keeper has liquidated the loan |
Only liquidator | You never see it from your own calls: only a keeper liquidates, after expiry. No price liquidation before expiry |
Position active. Use addToLoan | You already have a vault loan: use addToLoan() |
% by 10 / % range | partialLoanSellFor / hubPartialLoanSell need multiples of 10, range 10–100 |
cannot sell off with no profit | The collateral sold would not cover the debt it repays: repay instead |
Not user | partialLoanSellFor on a position that is not yours (or named on the wrong main) |
Invalid loan | No hub loan with that id for your wallet (a vault loan is extended on the vault: extendLoan(main, days, payInMainPair, refinance)) |
Prediction Market Errors (Quick Reference)
| Error | Fix |
|---|---|
(no reason string) on createMarket | The seed is under the host's floor (minSeed() × sizesFor(host, main, n).unit; on the private host minSeedPublic() / minSeedPrivate() × unit) or not a multiple of sizesFor(...).seedStep |
Already proposed | Dispute if you disagree; otherwise wait for quiet period |
Not an authorized voter | Only wallets on the resolver's panel (authorizedVoter(wallet)) vote |
Proposer cannot dispute self | Another wallet must dispute |
Too early to propose | Wait for the market's end time (15 minutes after it for anyone but the creator) |
Betting closed | The market's end time has passed |
| (no reason string) on a bet | The market is resolved: call redeem(market, fractionBps, slices, minOut) if you hold winning shares |
The full resolver table is in Module 14.
6. Error Recovery Decision Tree
Got an error?
│
├── AMOUNTS? (min, insufficient, below, seed)
│ └── Check: balance, minimum thresholds, seed floor and step for markets (`sizesFor`)
│
├── TIME? (duration, too short, too early, expired, deadline)
│ └── Check: loan remaining days (≥10), vesting cliff, market phases
│
├── STATE? (already, inactive, active, resolved, bonded)
│ └── Check: current state before acting: loan.active, presaleComplete, market status
│
├── PERMISSIONS? (only, unauthorized, not registered, not whitelisted)
│ └── Right wallet? (creator vs beneficiary) Authenticated? Token frozen? Authorized voter?
│
├── SLIPPAGE? (min out, shares not met)
│ └── Quote with tax, then apply your slippage tolerance. Reduce size if still failing.
│
└── GAS? (out of gas, not enough ETH)
└── Keep an ETH balance for gas.
Pre-Flight Checks
Before any loan operation:
- Is
loan.activetrue? - How many days remaining? (
loan.liquidationTime - now) - If remaining < 10 days: extend first
Before buying:
- Enough of the pair asset? (
balanceOf()) - SWAP picks the presale or the AMM itself. During a presale of a frozen token, check you are whitelisted
- Quote with tax (
getAmountsOut(amount, path)excludes the fee)
Before selling:
- Enough tokens? (
balanceOf()) - Staked main tokens:
unstakeis capped byfreeToUnstake(main, you) - Quote with tax (
getAmountsOut(amount, path)excludes the fee)
Before creating a market:
- Seed at least the host's floor (
minSeed()×sizesFor(host, main, n).unit; private host:minSeedPublic()/minSeedPrivate()× unit) and a multiple ofsizesFor(...).seedStep? - End time 0, or at least one minute ahead, and the same end time the moderator approval was signed for?
- Approval signature still before its
deadline?trancheBPvalid?msg.valueequal to the factory fee?
Before creating a token:
- Multiplier 1–100?
presaleTokensin whole tokens (above 0 if frozen; on a reversed token, Meme+, Yolo+ or Yolo, at most half the launch supply)?- startLP 1,000–100,000?
msg.valueequal tofeeAmount()(0 if fee-whitelisted)?autoVestDuration1–990 days when auto-vest is on?- On
createTokenFor:supplyMultiplierbetween 1 and the main'sMAX_SUPPLY_MULTIPLIER()?
Before voting/resolving:
authorizedVoter(you)is true? Only the panel votes.- Market in dispute phase (
inDispute(marketToken))? - Not in veto (
inVeto(marketToken))?