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


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:

KeyContractPurpose
chain, chainId, network, deploymentBlock-The network of this deployment
usdbthe default main's pair assetPair asset of the default main (USDG on Robinhood Chain; the key keeps its old name) (→01)
mainTokenA_STASISTOKENThe default main (every main is listed in mains[])
swapASwapEvery buy, sell, leverage buy, partial loan sell and cross swap
factoryATokenFactoryToken creation
stakingAStasisVaultThe 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
loanHubALOAN_HUBHub loans (a leverage position is recorded on its token's main, not here)
vestingA_VestingContractVesting schedules and vesting loans
taxesATaxesFees, the creator and rewards fee setters, on-chain referrals
marketTrading (alias tranche)ATrancheMarketPublic market host and its order book
privateMarket (alias tranchePrivate)ATrancheMarketPrivatePrivate market host (creator-managed markets)
resolverAMarketResolverResolution of public markets
bondToken, bondDecimals-The token resolver bonds are posted in, and its decimals
trancheReader (also reader)TrancheReaderMarket read helper: every call takes the host first (→10)
redemptionReaderRedemptionReaderRedemption quotes
leverageALEVERAGELeverage simulation (views)
liquidatorALiquidatorLiquidation of expired loans and positions, keepers only
hopExecutorAHopExecutorThe outside-pool hops of a cross swap
upDownMultiPredictionOne contract for every Up/Down asset and timeframe (→08)
erc8004RegistryexternalOn-chain agent identity; the zero address means agent registration is off on that network
wrappedNativeexternalWrapped 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 by mains[].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.apiKey persists across restarts if you store it.


Configuration Options

All options are passed to BasisClient constructor or BasisClient.create.

OptionTypeDefaultDescription
privateKeystring-Wallet private key. Enables write operations and automatic SIWE auth.
apiKeystring-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.
rpcUrlstringthe SDK's preset for the deployment's chainRPC endpoint. Validated on connect: it must return the chainId published at /contracts.json.
apiDomainstringhttps://launchonbasis.comBase URL for the Basis API.
agentboolean 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):

PropertyTypeDescription
client.usdbAddressaddressThe default main's pair asset (USDG on Robinhood Chain)
client.mainTokenAddressaddressThe default main
client.stakingAddressaddressThe vault, one contract for every main (a stake, not a token)
client.addressesobjectEvery key of /contracts.json, mains[] included
client.mains()Promise<MainInfo[]>Every main: SWAP.pairCount(), pairList(i), pairs(main) → (pairAsset, staking, active)
client.publicClientPublicClientviem public client for read-only calls
client.walletClientWalletClientviem wallet client for write operations (requires privateKey)
client.walletClient.account.addressaddressYour wallet address
client.apiBasisAPIOff-chain API wrapper
client.apiKeystringAuto-provisioned API key (persistent, no expiry)

Python (snake_case):

PropertyTypeDescription
client.w3Web3web3.py instance for raw contract calls
client.wallet_addressstrYour wallet address
client.usdb_addressstrThe default main's pair asset (USDG on Robinhood Chain)
client.main_token_addressstrThe default main
client.api_keystrAuto-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.

TokenDecimalsExample
USDG (the default main's pair asset)65 * 10**6 = 5 USDG
Any other pair assetdecimals() of the assetparseUnits("5", decimals)
Bond tokenBOND_UNIT()-
Mains181 * 10**18 = 1 main token
Factory tokens181 * 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 .env files (add .env to .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 TypeLimitScope
API Key (/api/v1/*)60 req/minPer key
SIWE Session (core endpoints)30 req/minPer IP
Transaction Sync (/api/v1/sync)60 req/minPer 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 window
  • X-RateLimit-Remaining: requests left in current window
  • X-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

MethodHowLifetime
SIWE sessionclient.authenticate()Until browser/process closes
API keyPass apiKey to constructorNo expiry

For long-running agents: use API keys. BasisClient.create() auto-provisions a key: store it and pass it on restarts.


HTTP Status Codes

StatusMeaning
400Bad request (missing or invalid parameters)
401Not authenticated (missing or expired session/API key)
403Forbidden (not the owner or insufficient permissions)
404Resource not found
409Conflict (duplicate resource, e.g. metadata already exists)
422Validation failed (invalid signature, sync error)
429Rate 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_loan still works but is deprecated: it delegates to syncTransaction.


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:

  1. Check the receipt first: if it exists, the transaction went through
  2. If still pending after 60s, resubmit with higher gas
  3. 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

WhatMethodAlert When
Loan expirygetUserLoanDetails(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 liquidatedliquidationTime < now and activeRepay or extend now
Liquidated by a keeperthe loan's isLiquidatedtrue: claim the remainder within 365 days of the liquidation; after that the leftover may no longer be claimable
ETH gas balancegetBalance()Below your gas budget
Pair-asset balance, per mainbalanceOf() on the pair assetBelow your minimum threshold
Token feegetTaxRate(token, wallet)Above the rate you planned for
Prediction market statuson chain: getMarketData(market) (end time), hasProposal(market), inDispute(market), inVeto(market)A market you watch needs a proposal or is in dispute
Vault rewardsearned(main, wallet) on the vault, per mainWorth claiming or compounding
Bond token balance (resolvers)balanceOf() on BOND_TOKEN()Below requiredBond(market, wallet)
RPC healthgetBlockNumber()Timeout or stale block

Shutdown Procedure

  1. Stop opening new positions (halt trading loops)
  2. Repay loans before expiry: hub loans with LOANS.repayLoan(hubId, amount) (2^256 - 1 repays in full), a vault loan with repay(main) on the vault. After liquidationTime a 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.
  3. Close leverage positions with client.trading.partialLoanSellFor(main, positionId, 100n, true, minOut) (contract SWAP.partialLoanSellFor), per position and per main, or repay them: each position is recorded on its token's main, so call repayLoan(id, 2^256 - 1, true) there, or repay through SWAP with repayWith(main, id, pairAsset, amount, minPairOut, true, deadline). A hub loan is sold down with LOANS.hubPartialLoanSell(hubId, 100n, minOut), or repaid. Never pass a leverage id to hubPartialLoanSell: it reads the number as a hub id. With no hub loan of that number it reverts Invalid loan; with one it sells that hub loan instead. No view returns what a full close pays: build minOut from MainLens.getPartialLoanSellAmounts(main, ...) and a taxed sell quote, and leave room for the exit fee.
  4. Unstake: repay the vault loan (repay(main)), claim(main) the rewards, then unstake(main, amount, toMainPair, minMainPair); it is capped by freeToUnstake(main, you). There is no unlock step and no vote lock.
  5. 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/Down claim(asset, tf, roundId, fractionBps, slices, minOut) for settled rounds; claimTokens(vestingId) for vestings.
  6. Verify final state: run reconstructState() to confirm no orphaned positions

5. Cross-Cutting Errors

Gas Errors

Deadline

  • When: swapCross, repayWith, the hub's repayLoanWith, market buy and Up/Down bets take a deadline; buyTokens and sellTokens take 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:
ActionToken pulledSpender
Buy, leverage buy, cross swappath[0] (the pair asset, or the main)SWAP
Sellthe token soldSWAP
Hub loan: take, add collateralthe collateral tokenthe loan hub
Hub loan: repay, extend paying in the pair assetthe main's pair assetthe 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 callthe pair assetthe address you call (the SDK sets the approval)
Vault: stakethe mainthe vault
Vault: repay, extend paying in the pair assetthe pair assetthe vault
Vesting: create, add tokensthe vested tokenVESTING
Vesting loan: repaythe main's pair assetVESTING
Market: seed, order fillthe market's pair assetthe market's host
Market: betthe chosen input token (pair asset, main, market token or another BASIS token)the market's host
Resolver: propose, dispute, vetoBOND_TOKEN()the resolver
Up/Down betthe chosen input tokenthe 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.value must equal feeAmount() (0 for a fee-whitelisted caller) exactly, else Wrong fee.
  • Market creation: msg.value must equal the factory's feeAmount() (0 when the host is fee-whitelisted) exactly; a wrong value reverts with no message.
  • Vesting creation: charged only while feeEnabled is on and the caller is not whitelisted; needs msg.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() >= amount and 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 minOut from the taxed quote: getAmountsOut excludes the fee. With rate = getTaxRate(token, wallet), a buy quotes getAmountsOut(amountIn × (10000 − rate) / 10000, path) and a sell getAmountsOut(amount, path) × (10000 − rate) / 10000 (Module 04). Then apply your slippage tolerance. For swapCross, quote with quoteCross(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 by presaleComplete(). 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

StatusMeaningFix
401Not authenticatedRe-create client with BasisClient.create() to refresh API key
403Forbidden / not the ownerVerify wallet is correct (creator vs beneficiary)
404Resource not foundVerify IDs and addresses
409Conflict / duplicateResource already exists: check before creating
429Rate limit exceededBack 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)

ErrorFix
incorrect loan durationDays must be between minDaysLoan() and maxDaysLoan() of the main (10 and 1000 today); an extension adds at least minDaysLoan()
Duration too shortRemaining time < 10 days: extend first, then add collateral
Minimum loan amount not metThe 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 activeLoan was liquidated or repaid: check loan.active first
Loan is still activeclaimLiquidation before a keeper has liquidated the loan
Only liquidatorYou never see it from your own calls: only a keeper liquidates, after expiry. No price liquidation before expiry
Position active. Use addToLoanYou already have a vault loan: use addToLoan()
% by 10 / % rangepartialLoanSellFor / hubPartialLoanSell need multiples of 10, range 10–100
cannot sell off with no profitThe collateral sold would not cover the debt it repays: repay instead
Not userpartialLoanSellFor on a position that is not yours (or named on the wrong main)
Invalid loanNo 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)

ErrorFix
(no reason string) on createMarketThe 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 proposedDispute if you disagree; otherwise wait for quiet period
Not an authorized voterOnly wallets on the resolver's panel (authorizedVoter(wallet)) vote
Proposer cannot dispute selfAnother wallet must dispute
Too early to proposeWait for the market's end time (15 minutes after it for anyone but the creator)
Betting closedThe market's end time has passed
(no reason string) on a betThe 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:

  1. Is loan.active true?
  2. How many days remaining? (loan.liquidationTime - now)
  3. If remaining < 10 days: extend first

Before buying:

  1. Enough of the pair asset? (balanceOf())
  2. SWAP picks the presale or the AMM itself. During a presale of a frozen token, check you are whitelisted
  3. Quote with tax (getAmountsOut(amount, path) excludes the fee)

Before selling:

  1. Enough tokens? (balanceOf())
  2. Staked main tokens: unstake is capped by freeToUnstake(main, you)
  3. Quote with tax (getAmountsOut(amount, path) excludes the fee)

Before creating a market:

  1. Seed at least the host's floor (minSeed() × sizesFor(host, main, n).unit; private host: minSeedPublic() / minSeedPrivate() × unit) and a multiple of sizesFor(...).seedStep?
  2. End time 0, or at least one minute ahead, and the same end time the moderator approval was signed for?
  3. Approval signature still before its deadline? trancheBP valid? msg.value equal to the factory fee?

Before creating a token:

  1. Multiplier 1–100?
  2. presaleTokens in whole tokens (above 0 if frozen; on a reversed token, Meme+, Yolo+ or Yolo, at most half the launch supply)?
  3. startLP 1,000–100,000?
  4. msg.value equal to feeAmount() (0 if fee-whitelisted)?
  5. autoVestDuration 1–990 days when auto-vest is on?
  6. On createTokenFor: supplyMultiplier between 1 and the main's MAX_SUPPLY_MULTIPLIER()?

Before voting/resolving:

  1. authorizedVoter(you) is true? Only the panel votes.
  2. Market in dispute phase (inDispute(marketToken))?
  3. Not in veto (inVeto(marketToken))?