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

Canonical addresses are published at https://launchonbasis.com/contracts.json. The SDK fetches this on startup and warns if hardcoded defaults are stale. All addresses are overridable via constructor options.

BSC Mainnet — Default Addresses:

ContractSDK NameAddressPurpose
USDBusdbAddress0x42bcF288e51345c6070F37f30332ee5090fC36BFProtocol stablecoin (→01)
STASIS (MAINTOKEN)mainTokenAddress0x3067ce754a36d0a2A1b215C4C00315d9Da49EF15Governance/utility token
SWAPswapAddress0x9F9cF98F68bDbCbC5cf4c6402D53cEE1D180715fAMM swap router — used by all trades and leverage
FACTORY (ATokenFactory)factoryAddress0xB6BA282f29A7C67059f4E9D0898eE58f5C79960DToken creation
STAKING (AStasisVault)stakingAddress0x1FE7189270fb93c32a1fEfA71d1795c05C41cb33wSTASIS vault
LOANS (LoanHub)loanHubAddress0xFe19644d52fD0014EBa40c6A8F4Bfee4Ce3B2449Loans (leverage lives on SWAP, not here)
VESTINGvestingAddress0xedd987c7723B9634b0Aa6161258FED3e89F9094CToken vesting schedules
TAXES (ATaxes)taxesAddress0x4501d1279273c44dA483842ED17b5451e7d3A601Surge tax management
PREDICTION (MarketTrading)marketTradingAddress0x396216fc9d2c220afD227B59097cf97B7dEaCb57Prediction market trading
RESOLVER (AMarketResolver)resolverAddress0xB5FFCCB422531Cf462ec430170f85d8dD3dC3f57Market resolution & voting
PRIVATE MARKETSprivateMarketAddress0x28675A82ee3c2e6d2C85887Ea587FbDD3E3C86EEPrivate prediction markets
Market ReaderreaderAddress0xF406cA6403c57Ad04c8E13F4ae87b3732daa087dRead-only market data helper (→10)
Leverage SimulatorleverageAddress0xeffb140d821c5B20EFc66346Cf414EeAC8A8FDB2Off-chain leverage simulation
Floor0x359dE659F0242352dD7F021c2EcB370284D95F45Floor price mechanism
ERC-8004 Identity Registry0x8004A169FB4a3325136EB29fA0ceB6D2e539a432On-chain agent identity

Canonical source: The SDK auto-fetches addresses from https://launchonbasis.com/contracts.json on startup and warns if hardcoded defaults are stale. Always trust that endpoint over any hardcoded values in documentation.

Naming note: MAINTOKEN is the contract/SDK variable name for STASIS. In code: client.mainTokenAddress (JS) / client.main_token_address (Python).

Constructor override names: factoryAddress, swapAddress, marketTradingAddress, loanHubAddress, vestingAddress, stakingAddress, resolverAddress, privateMarketAddress, readerAddress, leverageAddress, taxesAddress, usdbAddress, mainTokenAddress


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();
const price = await client.trading.getUSDPrice("0xTokenAddress...");

Python:

from basis_sdk import BasisClient

client = BasisClient()
price = client.trading.get_usd_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
const { parseUnits } = require("viem");
const result = await client.trading.buy("0xTokenAddress...", parseUnits("5", 18));
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"
)

result = client.trading.buy("0xTokenAddress...", 5_000_000_000_000_000_000)
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
privateKeystringWallet private key. Enables write operations and automatic SIWE auth.
apiKeystringAPI 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.
rpcUrlstringhttps://bsc-dataseed.binance.org/Custom BSC RPC endpoint. Validated on connect — must return chainId 56.
apiDomainstringhttps://launchonbasis.comBase URL for the Basis API.
agentboolean or objectERC-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.usdbAddressaddressUSDB contract address
client.mainTokenAddressaddressSTASIS/MAINTOKEN contract address
client.stakingAddressaddresswSTASIS vault contract address
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_addressstrUSDB contract address
client.main_token_addressstrSTASIS/MAINTOKEN contract address
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. All Basis tokens use 18 decimals — no exceptions.

TokenDecimalsExample
USDB185 * 10**18 = 5 USDB
STASIS181 * 10**18 = 1 STASIS
Factory tokens181 * 10**18 = 1 token

JavaScript:

import { parseUnits, formatUnits } from "viem";

const usdbRaw = parseUnits("5", 18);       // 5000000000000000000n
const humanUsdb = formatUnits(5000000000000000000n, 18);  // "5"

Python:

usdb_raw = 5 * 10**18                        # 5000000000000000000
# or via web3:
from web3 import Web3
usdb_raw = Web3.to_wei(5, "ether")
human = Web3.from_wei(5000000000000000000, "ether")  # 5

Exception: sellPercentage() takes a 1–100 integer, 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 a small BNB balance as a gas fallback (~0.005 BNB)

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)20 req/minPer IP

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, claim USDB from faucet, fund BNB 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, earn points 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. USDB balance
  const usdbBalance = 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. BNB gas balance
  const bnbBalance = await client.publicClient.getBalance({ address: wallet });
  if (bnbBalance < parseUnits("0.005", 18)) {
    console.warn("Low BNB - refill for gas");
  }

  // 4. Faucet check
  try {
    const faucetStatus = await client.api.getFaucetStatus();
    if (faucetStatus.canClaim) console.log(`Faucet available: ${faucetStatus.dailyAmount} USDB`);
  } catch (e) { /* non-critical */ }

  // 5. Loan expiry
  const loanCount = await client.loans.getUserLoanCount(wallet);
  for (let i = 1n; i <= loanCount; i++) {
    const loan = await client.loans.getUserLoanDetails(tokenAddress, 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

BSC uses 3-second blocks with occasional 1–3 block reorgs:

  • 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(tokenAddress, wallet, i);
    if (loan.active) state.loans.push({ hubId: i, ...loan });
  }

  // 2. Leverage positions
  const levCount = await client.loans.getLeverageCount(client.mainTokenAddress, wallet);
  for (let i = 1n; i <= levCount; i++) {
    const pos = await client.loans.getLeveragePosition(client.mainTokenAddress, wallet, i);
    if (pos.active) state.leveragePositions.push({ positionId: i, ...pos });
  }

  // 3. Staking position
  const shares = await client.publicClient.readContract({
    address: client.stakingAddress,
    abi: [{"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"}],
    functionName: 'balanceOf',
    args: [wallet],
  });
  const stasisValue = await client.staking.convertToAssets(shares);
  state.staking = { shares, stasisValue };

  // 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

The default public endpoint (bsc-dataseed.binance.org) has no SLA — ~10–20 req/s before throttling. For production agents:

Recommended providers (BSC):

  • Ankr — free tier available, good BSC support
  • QuickNode — fast and reliable, paid
  • NodeReal — BSC-focused meganode architecture
  • Chainstack — dedicated nodes available

Failover pattern:

const RPC_ENDPOINTS = [
  "https://your-primary-rpc.com",
  "https://bsc-dataseed1.binance.org",
  "https://bsc-dataseed2.binance.org",
];

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", 18));
}

// Wrong — parallel sends cause nonce collisions
// await Promise.all(tokens.map(t => client.trading.buy(t, amount)));

Monitoring Checklist

WhatMethodAlert When
Loan expirygetUserLoanDetails()liquidationTime< 24 hours remaining
Leverage expirygetLeveragePosition()liquidationTime< 24 hours remaining
BNB gas balancegetBalance()< 0.005 BNB
USDB operating balancebalanceOf() on USDB contractBelow your minimum threshold
Faucet eligibilitygetFaucetStatus()canClaim: true
Surge taxgetCurrentSurgeTax(token)> 0 on actively traded tokens
Prediction market statusgetDisputeData(marketToken)awaiting_proposal status
Staking lockTrack VOTE_LOCK_DURATION after votesCannot unstake for 24h after voting
RPC healthgetBlockNumber()Timeout or stale block

Shutdown Procedure

  1. Stop opening new positions (halt trading loops)
  2. Repay active loans before expiry (avoid collateral burn)
  3. Close leverage positions: client.loans.hubPartialLoanSell(tokenAddress, loanIndex, 100) (100% = full close)
  4. Unstake: unlock()sell() (only if not vote-locked)
  5. Claim pending rewards: claimLiquidation(hubId) for each expired loan, claimBounty(marketToken) for resolved markets
  6. Verify final state: run reconstructState() to confirm no orphaned positions

5. Cross-Cutting Errors

Gas Errors

GASLESS_REJECTED

  • When: Any MegaFuel-sponsored transaction
  • Why: Daily sponsorship limit reached (0.001 BNB/wallet/day) or contract not whitelisted
  • Fix: Use your own BNB for gas, or wait until the next day
  • Prevention: Keep ~0.005 BNB as fallback

Deadline expired

  • When: Swap operations with a deadline parameter
  • Why: Transaction took too long to mine
  • 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

  • When: Any operation requiring token approval
  • Why: Contract couldn't get spending approval (usually a gas issue)
  • Fix: Manually approve the contract to spend your tokens, then retry
  • Prevention: The SDK handles approvals automatically — if this appears, check gas

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 / Slippage exceeded / min shares not met

  • Fix: Use getAmountsOut(amount, path) first, set minOut to ~95% of expected output. If still failing, reduce amount.

Insufficient balance

  • Fix: Check balanceOf() before transacting

Token is frozen

  • Fix: Token is whitelist-only. Check whitelisted[yourAddress] before buying frozen tokens.

has not bonded yet / Token has already bonded

  • Fix: Check token.hasBonded. During reward phase → use buyBondingTokens() / sellBondingTokens(). After reward phase → use AMM (buy()/sell()). Both phases run through AMMs — the methods are just different entry points.

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 10–1000
Duration too shortRemaining time < 10 days — extend first, then add collateral
Minimum loan amount not metCheck getCollateralValue() — must be ≥ ~$0.99
Loan not activeLoan was liquidated or repaid — check loan.active first
Cannot liquidate before time endsBy design — no price liquidation before expiry
Position active. Use increaseLoanAlready have a vault loan — use increaseLoan()
% by 10 / % rangepartialSell() requires multiples of 10, range 10–100

Prediction Market Errors (Quick Reference)

ErrorFix
Seed must be in increments of 10 USDRound seed to nearest $10 increment
Already proposedDispute if you disagree; otherwise wait for quiet period
Must stake 5 tokens to voteStake ≥ 5 STASIS in resolver contract first
Stake locked due to recent voteWait for lastVoteTime + VOTE_LOCK_DURATION
market resolvedCall redeem() if you hold winning shares

6. Error Recovery Decision Tree

Got an error? │ ├── AMOUNTS? (min, insufficient, below, seed) │ └── Check: balance, minimum thresholds, increment rules ($10 for markets) │ ├── TIME? (duration, too short, too early, expired, deadline) │ └── Check: loan remaining days (≥10), surge quota, vesting cliff, market phases │ ├── STATE? (already, inactive, active, resolved, bonded) │ └── Check: current state before acting — loan.active, hasBonded, market status │ ├── PERMISSIONS? (only, unauthorized, not registered, not whitelisted) │ └── Right wallet? (creator vs beneficiary) Authenticated? Token frozen? │ ├── SLIPPAGE? (min out, shares not met) │ └── Simulate first. Set minOut to ~95% of expected. Reduce size if still failing. │ └── GAS? (GASLESS_REJECTED, ETH fail) └── Keep ~0.005 BNB fallback. Retry with own gas. Wait 24h for sponsorship reset.

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 USDB? (balanceOf())
  2. Has the token's reward phase completed? (hasBonded → determines which buy path)
  3. Is the token frozen? (need whitelist)
  4. Simulate first (getAmountsOut(amount, path))

Before selling:

  1. Enough tokens? (balanceOf())
  2. wSTASIS locked by vault loan? (can't sell)
  3. Simulate first (getAmountsOut(amount, path))

Before creating a market:

  1. Seed in $10 increments?
  2. Seed ≥ minimum? (differs for public vs private)
  3. End time in the future or 0?

Before creating a token:

  1. Multiplier 1–100?
  2. Reward-phase allocation 0–150,000 (≥1 if frozen)?
  3. startLP 100–10,000?

Before voting/resolving:

  1. Staked ≥5 STASIS in resolver?
  2. Market in dispute phase (inDispute[marketToken])?
  3. Not in veto (inVeto[marketToken])?