Module 04: Trading
Buy tokens, sell tokens, open leveraged positions, simulate before executing.
Prerequisites
- Wallet funded with the pair asset of the main you trade (USDG on the default main) (→01), client initialized (→02)
- Understand hybrid multiplier + presale (→11) before sizing trades
Next steps after a trade
- Bought STASIS? Stake it (→06), then borrow against it (→05)
- Bought Floor+? Borrow against it (→05) for capital efficiency
- Leverage is terminal: use it last in any stack (→12 for full strategies)
- Hit an error? See the error reference below or SDK Reference for the full error index
Examples use the default main, STASIS, whose pair asset is USDG (6 decimals). On another main read "USDG" as that main's pair asset and use its
decimals(). The mains of a deployment are listed in/contracts.json(mains[]).
1. Why Trade on BASIS
BASIS token mechanics differ fundamentally from standard AMMs:
- Stable+ tokens can only go up, measured in the main's pair asset. Elastic supply, and slippage stays in the pool, so every trade increases the price. STASIS is the canonical Stable+. No creator can rug a token: there is no LP to pull, and every token in circulation was purchased at market price.
- Floor+ tokens have a rising floor. Price moves freely above a floor that never decreases. Sell pressure creates a dip, not a death spiral. Your downside shrinks over time as the floor rises.
- Meme+ (reversed) tokens can fall in price. Their floor is never below the launch price; at setting 100 the floor is the launch price.
- No price liquidation on leverage. Leverage positions expire by time only: price crashes cannot trigger liquidation. Your leverage position is recorded on the token's main and has its own ids (§3c).
- Each main's vault earns from its own pair. 10% of the base fee of every trade on a main's tokens goes to that main's vault, for its stakers. There can be several mains, each with its own pair asset and its own pool in the vault.
- Real money. Every trade moves real funds and is final. Quote with the fee included before you send.
- Gas is paid in ETH.
2. How Trading Works
Routing
All trades go through one SWAP contract. SWAP keeps a registry of mains: pairs(main) → (pairAsset, staking, active), listed by pairList(i) and pairCount(). A token trades against its own main (token.quoteToken()), and a main trades against its pair asset.
| Trade | Path |
|---|---|
| Buy / sell a main | [pairAsset, main] / [main, pairAsset] |
| Buy / sell a factory token with the pair asset | [pairAsset, main, token] / [token, main, pairAsset] |
| Buy / sell a factory token with its main | [main, token] / [token, main] |
| Token to token, across mains, an outside token, the native coin | swapCross(path, amountIn, minOut, recipient, deadline, unwrapNative) in one transaction; quote with quoteCross(path, amountIn, trader) by eth_call (see Cross-pair swap) |
A path that does not match the token's main reverts (Not pair asset, Wrong main, Not main, Bad pair or unknown pair). A factory token paired straight with the pair asset ([pairAsset, token], [token, pairAsset]) reverts Bad pair.
A factory-token trade also moves its own main's pool, and that main's vault takes its 10% of the base fee.
Token Types
| Type | Price Behavior | Fee | Leverage |
|---|---|---|---|
| Stable+ (multiplier 100; every main is one) | Can only go up (in pair-asset units) | A main: the base only. A factory token: base + its creator fee + its rewards fee | Up to about 37x, depending on the token and the size of the buy; must be more than 2x |
| Utility+ (multiplier 50–99) and Floor+ (multiplier 1–49) | Free above rising floor | Base + creator fee + rewards fee | Lower: the loan is valued at the floor |
| Meme+, Yolo+ and Yolo (reversed curve: 1–50, 51–99, 100) | Free; the floor is never below the launch price | Base + creator fee + rewards fee (rewards-fee cap 10%) | Lowest: valued at the floor |
| Predict+ (market token, multiplier 100) | Price only up (Stable+ subtype) | Base + the market creator's creator fee; no rewards fee | As Stable+ |
Read the rate for your wallet: TAXES.getTaxRate(token, wallet) (basis points). Leverage reaches up to about 37x, depending on the token and the size of the buy. The position must be more than 2x (Under 2x); read simulateLeverageLocked / simulateLeverageFactoryLocked for the real figure.
Presale, then Open Trading
A token created with presaleTokens > 0 starts in a presale: presaleComplete() is false until presaleSold() reaches presaleTarget() (both are token counts with 18 decimals); the buy that reaches it emits Bonded. There is no presale method to choose: SWAP.buyTokens / sellTokens send the trade to the presale or the AMM by themselves, on the same curve. On a token created with auto-vest, a presale buy goes into a vesting schedule instead of the wallet.
Holder rewards are a separate thing: only a token created with rewardsEnabled pays its rewards fee to holders, in proportion to their balance, in the pair asset, claimable with claimRewards(). On any other token getClaimableRewards reads 0. claimRewards() never pays the creator.
AMM Pricing
BASIS prices every token on a proprietary pricing curve. The hybridMultiplier (1–49 for Floor+, 50–99 for Utility+, 100 for Stable+; Meme+, Yolo+ and Yolo use the same 1–100 dial on the reversed curve) sets where the token sits on the stability dial, for both buys and sells. This is not a standard AMM with a bolt-on sell mechanism, so every trade behaves differently from a traditional AMM.
- Stable+ (multiplier=100): Slippage stays in the pool, so the price can only go up in its pair asset: both buys and sells increase the price.
- Utility+ and Floor+ (multiplier 50–99 and 1–49): The price moves freely, and slippage that stays in the pool builds a rising floor. Even at multiplier 1, tokens are far more stable than on a traditional AMM.
Implication for agents: Standard AMM arbitrage assumptions do not apply. On Stable+ tokens, price increases on every trade regardless of direction. On Floor+ tokens, the floor rises with every trade. Model your strategies around these mechanics.
Fees and Where They Go
Every trade and every bet pays one base fee, TAXES.baseRate(): 0.60% today (capped at 1.5% in the contract). A wallet with an on-chain fee referrer pays 10% less (0.54%). A factory or market token adds its creator fee (0 until its creator sets it, at most 1%) and, only on a token created with holder rewards, its rewards fee (at most maxRewardsFee: 0.5% at multiplier 100, rising to 10% at multiplier 1, always 10% on a reversed token (Meme+, Yolo+, Yolo)). A main pays the base only. A fee-exempt wallet pays nothing. Each of the two fees can change once per cooldown (4 hours today). Read the live numbers: getTaxRate(token, wallet) (basis points) and getTokenTaxInfo(token).
Trades and bets: split of the base part:
| Recipient | Share of the base fee |
|---|---|
| SOCIAL (Alpha+ author pay) | 15% on trades through SWAP; on a bet this 15% goes to the buyback, to be burned |
| Vault of the token's main | 10% |
| Operating wallet | 10% |
| The trader's fee referrer | 15% |
| That referrer's referrer | 5% |
| Buyback | the rest (45%, plus any referral share nobody can receive: 65% with no referrer) |
Loan-side fees and unwon prize money (loan origination, increase and extension, the leverage loop fees and the unused last slice of a leverage open, the 3% leverage exit tax):
| Recipient | Share of the base fee |
|---|---|
| Vault of the token's main | 10% |
| Operating wallet | 10% |
| Creator of the collateral token | 20% |
| Buyback | 60% |
The creator fee goes to the token's dev wallets on every trade, and the rewards fee to the token's holders; nothing is claimed by the creator. Payouts are in the pair asset of the token's main. A bet pays getTaxRate(marketToken, bettor).
3. Actions
Setup used by the examples below:
// The default main and its pair asset; on another main take them from /contracts.json mains[]
const MAIN = client.mainTokenAddress;
const PAIR_ASSET = client.usdbAddress;
const pairDecimals = await client.publicClient.readContract({ address: PAIR_ASSET, abi: erc20Abi, functionName: "decimals" }); // 6 for USDG
MAIN = client.main_token_address
PAIR_ASSET = client.usdb_address
pair_decimals = 6 # read decimals() from the pair asset; 6 for USDG
3a. Buy: client.trading.buy()
Contract call: SWAP.buyTokens(amount, minOut, path, wrapTokens); approve path[0] to SWAP.
// JS
const result = await client.trading.buy("0xTokenAddress", parseUnits("5", pairDecimals));
// With slippage protection (recommended). getAmountsOut excludes the trade tax:
// quote the input after the tax.
const amountIn = parseUnits("5", pairDecimals);
const taxBps = await client.taxes.getTaxRate(TOKEN, wallet);
const net = amountIn - amountIn * BigInt(taxBps) / 10000n;
const preview = await client.trading.getAmountsOut(net, [PAIR_ASSET, MAIN, TOKEN]);
const minOut = preview * 98n / 100n; // 2% tolerance on a quote that already has the tax off
const result = await client.trading.buy(TOKEN, amountIn, minOut);
# Python
result = client.trading.buy("0xTokenAddress", 5 * 10**pair_decimals)
# With slippage protection (getAmountsOut excludes the trade tax)
amount_in = 5 * 10**pair_decimals
tax_bps = client.taxes.get_tax_rate(TOKEN, wallet)
net = amount_in - amount_in * tax_bps // 10000
preview = client.trading.get_amounts_out(net, [PAIR_ASSET, MAIN, TOKEN])
min_out = preview * 98 // 100
result = client.trading.buy(TOKEN, amount_in, min_out)
SWAP.quoteCross(path, amountIn, wallet) called with eth_call returns the same taxed figure in one read.
| Param | Type | Description |
|---|---|---|
tokenAddress | string | Token to buy |
amountIn | bigint/int | Pair-asset amount of the token's main, in that asset's decimals (6 for USDG) |
minOut | bigint/int | Min tokens to receive (slippage guard). Never pass 0 in production |
wrapTokens | boolean | Only when buying a main: stake the bought tokens in that main's vault in the same transaction. Reverts Below min stake when the bought amount is under the vault's minStake(). Ignored for factory tokens. Default: false |
Pre-flight checklist:
- Check your balance of the pair asset
- Check pool depth: Call
client.api.getToken(tokenAddress)and compare your trade size againstdepthUsdE18(divide by 1e18). If your trade exceeds 5% of pool depth, run the impact probe (Section 7) or split into smaller trades. Elastic supply AMMs are more sensitive to large orders than traditional AMMs: use 5% as the sizing threshold. - Quote with the tax included (above)
- Set
minOutto ~95–98% of that taxed quote - Read the fee:
client.taxes.getTaxRate(tokenAddress, wallet)(basis points). On top of the base a creator can set a creator fee of up to 1% and, on a rewards token, a rewards fee of up to 10%.
What happens: the pair asset goes to SWAP, the tax is taken off the input, and the tokens are bought on the curve (elastic supply) and sent to you. If Stable+, price increases permanently. During a presale the buy counts toward presaleSold; if the token has auto-vest on, a presale buy is put in a vesting schedule for you instead of being delivered. Holding a rewards-enabled token earns its rewards fee (→07).
Pool depth ≠ tradability. BASIS uses elastic supply: tokens are minted on buy and burned on sell. All factory tokens are tradeable from creation.
depthUsdE18fromgetToken()represents the pool depth that determines price impact, not whether trading is possible. A token with $100 liquidity is still fully tradeable: your price impact will just be higher per dollar spent.
Fee: TAXES.getTaxRate(token, wallet): the 0.60% base (0.54% with a fee referrer) plus the token's creator fee and rewards fee.
→ For trades exceeding 5% of pool depth, see Section 7 (Position Sizing) before executing. Elastic supply AMMs are more sensitive to large orders than traditional AMMs. Large trades relative to depth move the price sharply. Probe first, split if needed.
3b. Sell: client.trading.sell() and sellPercentage()
Contract call: SWAP.sellTokens(amount, minOut, path, false); approve the token sold to SWAP.
// JS: sell exact amount
const result = await client.trading.sell("0xTokenAddress", parseUnits("1", 18), true); // true = sell through to the pair asset
// JS: sell percentage (reads balance automatically)
const result = await client.trading.sellPercentage("0xTokenAddress", 50); // sell 50%
# Python: sell exact amount
result = client.trading.sell("0xTokenAddress", 1 * 10**18, to_pair_asset=True)
# Python: sell percentage
result = client.trading.sell_percentage("0xTokenAddress", 50)
sell() parameters:
| Param | Type | Description |
|---|---|---|
tokenAddress | string | Token to sell |
amount | bigint/int | Token amount (18 decimals) |
toPairAsset | boolean | Sell through to the main's pair asset ([token, main, pairAsset]). false stops at the main ([token, main]). Default: false |
minOut | bigint/int | Min output. Never pass 0 in production |
Native coin out goes through swapCross with a path that ends in the wrapped native coin and unwrapNative = true, only where a route from the pair asset to the wrapped native coin exists. sellTokens(…, swapToETH = true) reverts use swapCross. The sell fee is taken from the output.
sellPercentage() parameters:
| Param | Type | Description |
|---|---|---|
tokenAddress | string | Token to sell |
percentage | number | 1–100 |
toPairAsset | boolean | Sell through to the pair asset. Default: false |
Pre-flight checklist:
- Check token balance (
client.tokens.balanceOf(tokenAddress, wallet)) - A staked main is not in your wallet: unstake it first (Module 06)
- Preview:
getAmountsOut(amount, path), then takegetTaxRate(tokenYouSell, wallet)basis points off the result
Token-specific sell behavior:
- Stable+: You CAN sell, and the price stays up for other holders. Your sale still receives less than the quoted price after fees and slippage, more so on a large sale.
- Floor+: Price dips on sell, but floor holds. Other holders are protected from cascade selling.
- Meme+: The price falls on a sell; the floor never goes below the launch price.
Sell vs Borrow decision:
- Sell when the price is right and you want to lock in profit or fully exit the position
- Borrow when you want liquidity but still believe in the upside: keeps your exposure intact
- Good operators use both. Borrowing is powerful but delaying an exit on a peaked token is not a strategy.
3c. Leverage Buy: client.trading.leverageBuy()
⚠️ Leverage is not a hub loan.
leverageBuy()opens an amplified position in one transaction (SWAP.leverageBuy(amount, minOut, path, numberOfDays), ormixedBuyto borrow against only part of the first buy: the rest stays in the same position as debt-free collateral, nothing goes to the wallet). Your leverage position is recorded on the token's main, one set per main:leverages(wallet, id)andleverageCount(wallet), ids from 1. A wallet has at most one open position per token (openLeverageOf(wallet, token)on the main, 0 = none): a leverage buy on a token where you already hold one adds to it and keeps its end date. Hub-loan ids and leverage ids are separate counters: never pass a leverage id to aclient.loansmethod. Partial-close withSWAP.partialLoanSellFor(main, id, percentage, true, minOut). To extend, repay or claim a liquidated position, call that main withisLeverage = true(below).
Always simulate before executing.
// JS: Step 1: Simulate
const sim = await client.leverageSimulator.simulateLeverageLocked(
parseUnits("10", pairDecimals),
[PAIR_ASSET, MAIN],
10n, // minDaysLoan() of the main (10 today)
100n // leveragePct: 100 = all of it leveraged (leverageBuy); 1 to 99 quotes a mixedBuy
);
console.log("Total position:", sim.totalColleteral); // the field is spelled totalColleteral in the ABI
console.log("Total borrowed:", sim.totalBorrowed);
console.log("Total fees:", sim.totalFees);
// Step 2: Execute. leverageBuy checks minOut against the whole position
const minOut = sim.totalColleteral * 97n / 100n;
const result = await client.trading.leverageBuy(parseUnits("10", pairDecimals), minOut, [PAIR_ASSET, MAIN], 10n);
// Take the position id from the LeverageCreated event in the receipt (LeverageIncreased when the buy
// added to your open position on that token), or read openLeverageOf(wallet, token) on the main.
// There is nothing to wait for.
# Python
sim = client.leverage_simulator.simulate_leverage_locked(10 * 10**pair_decimals, [PAIR_ASSET, MAIN], 10, 100)
print(f"Position: {sim.totalColleteral}, Fees: {sim.totalFees}, Borrowed: {sim.totalBorrowed}")
min_out = sim.totalColleteral * 97 // 100 # leverageBuy checks minOut against the whole position
result = client.trading.leverage_buy(10 * 10**pair_decimals, min_out, [PAIR_ASSET, MAIN], 10)
For a factory token (3-hop path):
// Use simulateLeverageFactoryLocked for factory tokens
const sim = await client.leverageSimulator.simulateLeverageFactoryLocked(
parseUnits("10", pairDecimals),
[PAIR_ASSET, MAIN, "0xFactoryToken..."],
10n,
100n
);
const result = await client.trading.leverageBuy(
parseUnits("10", pairDecimals), sim.totalColleteral * 97n / 100n,
[PAIR_ASSET, MAIN, "0xFactoryToken..."],
10n
);
leverageBuy() parameters:
| Param | Type | Description |
|---|---|---|
amount | bigint/int | Pair-asset input, in the pair asset's decimals |
minOut | bigint/int | Minimum total position (totalColleteral of the simulation less your tolerance) |
path | address[] | [pairAsset, main] or [pairAsset, main, factoryToken]; path[0] must be the pair asset of path[1]'s main, else wrong path |
numberOfDays | bigint/int | minDaysLoan() to maxDaysLoan() of the main (10 to 1000 today). Ignored when the buy adds to an open position on the token: the add is priced for the days that position has left |
Reverts: Under 2x (the position would not be more than twice a plain buy); Extend first (an add to an open position with fewer than minDaysLoan() days left); Range (mixedBuy with a percentageLeverage outside 1 to 99); In presale (an auto-vest token cannot be leveraged before its presale ends); No WL for freeze and WL cap (a frozen token needs a whitelist entry with room); Min out.
How it works internally (you don't need to manage this): Each loop buys tokens and pays the token's swap tax at its full rate (no referral discount), borrows against them (2% plus 0.005% per day, today's values), and buys again. The loops stop when the last slice is too small for another loop; that small slice is not bought but paid in as a loan-side fee. The contract handles the entire loop in a single atomic transaction. Either it fully succeeds or fully fails: no half-built positions. You interact with the result as a single leverage position, not as separate loans.
Leverage characteristics:
| Token Type | Effective Leverage | Why |
|---|---|---|
| Stable+ (STASIS) | Up to about 37x, depending on the size of the buy; read the simulation | Floor = spot (100% LTV), maximum loops |
| Floor+ near launch | High (varies) | Floor ≈ spot at launch: this window closes fast |
| Floor+ (spot >> floor) | Lower | LTV calculated against floor, not spot: fewer effective loops |
Leverage sizing rules:
- Smaller positions on deep pools = more loops = higher leverage
- Larger positions = fewer effective loops (price impact reduces each loan yield)
- The position must be more than 2x the spot amount, or the call reverts
Under 2x - Rule: Always take minimum duration (10 days) and extend. Extensions cost 0.005%/day vs 2% to re-originate (today's values; read
minDaysLoan(),staticFeePercentage()anddynamicFeePercentage()on the main).
At expiry nothing happens by itself. After liquidationTime a keeper liquidates the position (ALiquidator.liquidate; only the main's liquidator contract may do it). 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. The keeper closes the position: the collateral that covers the debt is taken to repay it, for every token type. 10% of the rest is the liquidation fee, and you claim the remainder with claimLiquidation(id, true) on the main, in the collateral token. Claim within 365 days of the liquidation; after that the leftover may no longer be claimable. Worst case nothing is left. You never owe more than the collateral. There is no price liquidation.
Repaying or extending. The contract calls (the SDK wraps them, and SWAP.repayWith repays with the pair asset through SWAP):
| Action | Contract call (on the position's main) | Wrapper |
|---|---|---|
| Extend | extendLoan(id, days, true, payInMainPair, refinance): days from minDaysLoan() to maxDaysLoan(), or 0 with refinance; with payInMainPair approve the pair asset to the main for the fee | client.trading.extendLeverage(main, id, days, payInMainPair, refinance) |
| Repay | repayLoan(id, amount, true): 2^256 - 1 repays all and returns all the collateral, less repays part and returns the same share of the collateral (what is left of the debt must stay at least the main's minimumLoan(), else Repay in full); approve leverages(wallet, id).fullAmount of the pair asset to the main first | client.trading.repayLeverage(main, id) |
| Claim after liquidation | claimLiquidation(id, true) | client.trading.claimLeverageLiquidation(...) |
Otherwise take profit or exit with partialLoanSellFor (§3d).
3d. Close / Partial Close: client.trading.partialLoanSellFor(main, …, isLeverage=true)
⚠️ This method lives on
client.trading, NOTclient.loans. Close withSWAP.partialLoanSellFor(main, id, pct, true, minOut), naming the main the position lives on. A hub loan is closed withLOANS.hubPartialLoanSell(Module 05). Passing100nas percentage fully closes the position: the "partial" in the name refers to the ability to close in 10% increments, not a constraint.
Close all or part of a leveraged position.
// JS: Read positions (1-indexed: loop from 1 to count inclusive).
// Reads leverageCount / leverages on the position's main: positions are per main, run the loop once per main.
const count = await client.trading.getLeverageCount(wallet);
for (let i = 1n; i <= count; i++) {
const pos = await client.trading.getLeveragePosition(wallet, i);
if (pos[9]) { // pos[9] = active flag
console.log(`Active position ${i}: ${pos[2]} tokens of ${pos[1]}`);
}
}
// Close a position: 100n = full close, or use 10-90 for partial
const positionId = 1n; // 1-indexed: find via the loop above
// Preview what the close really sells: the residual, the part of your collateral left to sell.
// The preview is on MainLens (caMAINLENS in /contracts.json), with the main as the first argument.
const [, , residualTokens] = await client.publicClient.readContract({
address: MAIN_LENS, abi: mainLensAbi, functionName: "getPartialLoanSellAmounts",
args: [MAIN, positionId, wallet, 100n, true],
});
const quote = await client.trading.getAmountsOut(residualTokens, [MAIN, PAIR_ASSET]); // a factory token: [token, MAIN, PAIR_ASSET]
const sellMinOut = quote * 95n / 100n; // no trade tax on the close; minOut is checked after the exit tax (at most 3%), so 3% for it plus 2% tolerance
const result = await client.trading.partialLoanSellFor(MAIN, positionId, 100n, true, sellMinOut);
# Python: Read positions (1-indexed), on the position's main
count = client.trading.get_leverage_count(wallet)
for i in range(1, count + 1):
pos = client.trading.get_leverage_position(wallet, i)
if pos[9]: # active flag
print(f"Active position {i}: {pos[2]} tokens of {pos[1]}")
# Close a position
position_id = 1
_, _, residual_tokens = main_lens.functions.getPartialLoanSellAmounts(MAIN, position_id, wallet, 100, True).call() # MainLens: caMAINLENS
quote = client.trading.get_amounts_out(residual_tokens, [MAIN, PAIR_ASSET]) # a factory token: [token, MAIN, PAIR_ASSET]
sell_min_out = quote * 95 // 100 # no trade tax on the close; minOut is checked after the exit tax (at most 3%)
result = client.trading.partial_loan_sell_for(MAIN, position_id, 100, True, sell_min_out)
The contract compares minOut with what you receive: the pair-asset proceeds of the residual (the part of your collateral left to sell), after the exit tax (3% of the part above your remaining own cash in, so at most 3% of the proceeds). The residual sale pays no swap tax. Quoting the whole collateral instead sets minOut far too high and reverts Min out not met on every real position.
| Param | Type | Description |
|---|---|---|
positionId | bigint/int | Leverage position index (1-indexed). Find via getLeverageCount() + getLeveragePosition() loop. Index 0 is always empty. |
percentage | bigint/int | Must be divisible by 10 (10, 20, 30 ... 100). 100 = full close. Any other value reverts % by 10 (not a multiple of 10) or % range (outside 10 to 100). A caller that does not own the position gets Not user. |
isLeverage | boolean | true for a leverage position. With false a wallet's call reverts Not user: a plain loan belongs to the hub and is sold with LOANS.hubPartialLoanSell. |
minOut | bigint/int | Minimum pair asset you receive from the residual, after the exit tax (see the preview above). Never pass 0. |
Exit tax: 3% of the proceeds above what is left of your own cash in (leverageBuyAmount − cashedOut). The result is in the LoanSold event.
getLeveragePosition return fields (leverages(user, id) on the main):
| Index | Field | Type | Description |
|---|---|---|---|
| 0 | user | address | Wallet that opened the position |
| 1 | token | address | Token held as collateral |
| 2 | collateralAmount | uint256 | Collateral held by the protocol until the position closes |
| 3 | liquidatedAmount | uint256 | Collateral taken by a liquidation to repay the debt |
| 4 | fullAmount | uint256 | Total repay obligation |
| 5 | borrowedAmount | uint256 | Original borrowed amount |
| 6 | liquidationTime | uint256 | Unix timestamp of expiry |
| 7 | liquidationClaim | uint256 | What the owner can claim after a liquidation |
| 8 | isLiquidated | bool | Whether position was liquidated |
| 9 | active | bool | Whether position is still open |
| 10 | creationTime | uint256 | Unix timestamp of creation |
| 11 | timeOfClosure | uint256 | Unix timestamp of closure |
| 12 | leverage | object | { leverageBuyAmount, cashedOut } |
Troubleshooting: Position returns all zeros? Leverage positions are 1-indexed and stored per main. If
getLeveragePosition(wallet, 0)returns all zeros, try index 1, and read the main the position was opened on. Loop from 1 togetLeverageCount()inclusive: index 0 is always empty/cleared.
4. Simulate Before Executing
All simulation methods are read-only and free to call.
Price Preview
// Preview output for any swap: returns a single bigint (scalar), not an array. Before tax.
const expectedOut = await client.trading.getAmountsOut(parseUnits("5", pairDecimals), [PAIR_ASSET, MAIN, TOKEN]);
expected_out = client.trading.get_amounts_out(5 * 10**pair_decimals, [PAIR_ASSET, MAIN, TOKEN])
# Returns a single int, not a list. Before tax.
Leverage Simulation
// A main (2-hop)
const sim = await client.leverageSimulator.simulateLeverageLocked(amount, [PAIR_ASSET, MAIN], days, 100n); // leveragePct 100 = leverageBuy
// Factory token path (3-hop)
const sim = await client.leverageSimulator.simulateLeverageFactoryLocked(amount, [PAIR_ASSET, MAIN, TOKEN], days, 100n);
simulateLeverageLocked() return fields (EndResult, ABI spelling; money fields are in the main's pair-asset units). Only the fields you need are listed; the other fields are internal.
| Field | Description |
|---|---|
totalRepay | Total you would need to repay to close the position |
totalBorrowed | Pair asset borrowed across all loops |
totalColleteral | Your total position size in tokens after all loops |
totalFees | Swap tax plus the loan fees on every loop |
totalDexTax | The swap-tax part of totalFees |
Position Inspection
// Current prices: no contract returns USD
const pairPrice = await client.trading.getPairPrice(tokenAddress); // raw pair-asset units per whole token (token.getPairPrice())
const mainPrice = await client.trading.getTokenPrice(tokenAddress); // in the token's main (for a main: in its pair asset)
// Existing leverage positions (1-indexed: loop 1 to count inclusive), per main
const count = await client.trading.getLeverageCount(walletAddress);
for (let i = 1n; i <= count; i++) {
const pos = await client.trading.getLeveragePosition(walletAddress, i);
if (pos[9]) console.log(`Position ${i}: ${pos[2]} of ${pos[1]}, expires ${pos[6]}`);
}
Live Reads for a Token
For a live token, read the live values instead of simulating raw state: SWAP.getAmountsOut for a quote, the token's own calculateFloor() for its floor price, client.trading.getPairPrice(token) for its price in the pair asset, and the MainLens previews (getPartialLoanSellAmounts) for a close. No contract returns a USD price.
5. Fees
| Action | Fee | Goes To |
|---|---|---|
| Buy / sell any token | getTaxRate(token, wallet): 0.60% base (0.54% with a fee referrer) + creator fee + rewards fee | Base: SOCIAL 15, vault 10, operating wallet 10, referrers 15 + 5, buyback the rest (45 to 65). Creator fee: the creator. Rewards fee: holders |
| Leverage open | The token's full swap tax on every loop + 2% origination per loop + 0.005%/day | Swap tax: as a trade, with no referral. Loan fees: vault 10, operating wallet 10, the token's creator 20, buyback 60 |
| Leverage exit | 3% of the cash-out above the initial buy | Same loan-side split |
Round-trip cost reality:
- Round trip = 2 ×
getTaxRate, plus price impact: 1.2% on a main or a token with no creator or rewards fee (1.08% with a fee referrer) - Leverage: read
totalFeesandtotalDexTaxfrom the simulation
Loan duration rule: Always take minimum duration (10 days) and extend as needed. A 100-day loan costs 2% + 0.5% = 2.5%. A 10-day loan extended 90 days costs the same 2.5% but gives you the option to exit early without re-originating. (Today's values: read staticFeePercentage(), dynamicFeePercentage(), minDaysLoan() and maxDaysLoan() on the main.)
6. Floor+ Profit Scenarios
Floor+ tokens are the most interesting to unwind. The floor rises over time, which can lock in gains even after price pullbacks.
| Scenario | Floor at Buy | Current Price | Best Move | Why |
|---|---|---|---|---|
| Token ran up big | $1.00 | $3.50 | Sell, repay loan | Captures full spread above entry |
| Token near floor | $1.00 | $1.10 | Hold, extend loan | The floor limits the fall: wait for volume |
| Token pulled back but floor rose | $1.00 (floor now $1.80) | $2.00 | Sell at $2.00 | Risen floor locked in gains through pullback |
| Token at floor, no volume | $1.00 | $1.00 | Let loan expire | Small loss, mostly the fees: the floor held |
Key insight: Loan LTV is calculated against the floor price, but you sell at the market price. When a Floor+ token runs up, there is a large spread between what you owe (floor-based) and what the token is actually worth. Selling captures that entire spread.
Leverage exit example: $100 input → $800 position at $1.00. Token rises to $2.00. Position worth about $1,600, debt about $700: about $900 comes out before the exit tax. The tax is 3% of the $800 above your own $100 (about $24), so about $876 reaches the wallet, before price impact. Use partialLoanSellFor() to scale out in 10% increments without closing the whole position.
7. Position Sizing
Large single buys on shallow pools move the price significantly. Always probe before committing.
// Probe price impact before entering
// getAmountsOut contains no fee: this probe measures curve impact only
const testAmount = targetAmount / 100n; // 1% probe
const testOut = await client.trading.getAmountsOut(testAmount, path);
const testRate = testOut * 100n / testAmount;
const fullOut = await client.trading.getAmountsOut(targetAmount, path);
const fullRate = fullOut * 100n / targetAmount;
const impactBps = (testRate - fullRate) * 10000n / testRate; // basis points
// < 50bp (0.5%): good, standard trade
// 50–200bp (0.5–2%): acceptable for conviction plays
// > 200bp (2%+): split into multiple smaller trades
Factors affecting impact:
startLP(a creator-set depth setting from 1,000 to 100,000, not deposited capital; 10,000 is the standard depth) sets pool depth, higher = less impact per trade- Stable+ pools only grow over time (slippage stays in the pool): impact decreases as the token matures
- Floor+ pools grow more slowly: impact decreases but less predictably
- A factory-token trade also moves its own main's pool, so that main's depth matters too
Get token details before trading:
// Use getToken(address) to inspect depthUsdE18 and multiplier
// depthUsdE18 (USD x 1e18) is the current pool depth: use it to size trades
8. Complete Examples
Example: Buy, Check Balance, Sell 50%
const { BasisClient } = require("basis-sdk-js");
async function tradeTokens() {
const client = await BasisClient.create({ privateKey: "0xYourKey..." });
const TOKEN = "0xTokenAddress...";
const MAIN = client.mainTokenAddress; // the default main
const PAIR_ASSET = client.usdbAddress; // its pair asset (USDG, 6 decimals)
const wallet = client.walletClient.account.address;
// Check price: raw pair-asset units per whole token
const price = await client.trading.getPairPrice(TOKEN);
console.log("Price:", price, "pair-asset units");
// Preview on the input after the tax (getAmountsOut excludes the trade tax)
const pairDecimals = 6; // USDG; on another main read /contracts.json mains[].pairDecimals
const amountIn = parseUnits("5", pairDecimals);
const taxBps = await client.taxes.getTaxRate(TOKEN, wallet);
const net = amountIn - amountIn * BigInt(taxBps) / 10000n;
const preview = await client.trading.getAmountsOut(net, [PAIR_ASSET, MAIN, TOKEN]);
// Buy with 2% slippage tolerance
const minOut = preview * 98n / 100n;
try {
const buy = await client.trading.buy(TOKEN, amountIn, minOut);
console.log("Bought:", buy.hash);
} catch (e) {
if (e.message.includes("Min out")) {
// Retry with 5% tolerance
const retryMin = preview * 95n / 100n;
const buy = await client.trading.buy(TOKEN, amountIn, retryMin);
console.log("Bought (retry):", buy.hash);
} else throw e;
}
// Sell 50%: reads balance automatically
const sell = await client.trading.sellPercentage(TOKEN, 50);
console.log("Sold 50%:", sell.hash);
}
from basis_sdk import BasisClient
def trade_tokens():
client = BasisClient.create(private_key="0xYourKey...")
TOKEN = "0xTokenAddress..."
MAIN = client.main_token_address # the default main
PAIR_ASSET = client.usdb_address # its pair asset (USDG, 6 decimals)
price = client.trading.get_pair_price(TOKEN)
print("Price:", price, "pair-asset units")
pair_decimals = 6 # USDG; on another main read /contracts.json mains[].pairDecimals
amount_in = 5 * 10**pair_decimals
tax_bps = client.taxes.get_tax_rate(TOKEN, client.wallet_address)
net = amount_in - amount_in * tax_bps // 10000
preview = client.trading.get_amounts_out(net, [PAIR_ASSET, MAIN, TOKEN])
min_out = preview * 98 // 100 # 2% slippage tolerance
buy = client.trading.buy(TOKEN, amount_in, min_out)
print("Bought:", buy["hash"])
sell = client.trading.sell_percentage(TOKEN, 50)
print("Sold 50%:", sell["hash"])
Example: Leverage: Simulate, Open, Partial Close
const { BasisClient } = require("basis-sdk-js");
async function leverageTrading() {
const client = await BasisClient.create({ privateKey: "0xYourKey..." });
const PAIR_ASSET = client.usdbAddress; // the default main's pair asset (USDG, 6 decimals)
const MAIN = client.mainTokenAddress; // the default main
const path = [PAIR_ASSET, MAIN];
const amount = parseUnits("10", 6);
// 1. Simulate
const sim = await client.leverageSimulator.simulateLeverageLocked(amount, path, 10n, 100n);
console.log("Position:", sim.totalColleteral, "Fees:", sim.totalFees, "Borrowed:", sim.totalBorrowed);
// 2. Open (min 10 days today); minOut is checked against the whole position
const minOut = sim.totalColleteral * 97n / 100n;
const open = await client.trading.leverageBuy(amount, minOut, path, 10n);
console.log("Opened:", open.hash);
// 3. Get position (1-indexed: loop from 1 to count inclusive), on this main
const wallet = client.walletClient.account.address;
const count = await client.trading.getLeverageCount(wallet);
let activeId;
for (let i = count; i >= 1n; i--) {
const pos = await client.trading.getLeveragePosition(wallet, i);
if (pos[9]) { activeId = i; break; } // pos[9] = active flag
}
// 4. Partial close: 50% (must be multiple of 10, 100 = full close)
const [, , residualTokens] = await client.publicClient.readContract({
address: MAIN_LENS, abi: mainLensAbi, functionName: "getPartialLoanSellAmounts", // MainLens: caMAINLENS
args: [MAIN, activeId, wallet, 50n, true],
});
const quote = await client.trading.getAmountsOut(residualTokens, [MAIN, PAIR_ASSET]);
const sellMin = quote * 95n / 100n; // no trade tax on the close; minOut is checked after the exit tax (at most 3%)
const close = await client.trading.partialLoanSellFor(MAIN, activeId, 50n, true, sellMin);
console.log("Partially closed:", close.hash);
}
from basis_sdk import BasisClient
def leverage_trading():
client = BasisClient.create(private_key="0xYourKey...")
PAIR_ASSET = client.usdb_address # the default main's pair asset (USDG, 6 decimals)
MAIN = client.main_token_address # the default main
path = [PAIR_ASSET, MAIN]
amount = 10 * 10**6
sim = client.leverage_simulator.simulate_leverage_locked(amount, path, 10, 100)
print(f"Position: {sim.totalColleteral}, Fees: {sim.totalFees}")
min_out = sim.totalColleteral * 97 // 100 # checked against the whole position
open_result = client.trading.leverage_buy(amount, min_out, path, 10)
print("Opened:", open_result["hash"])
count = client.trading.get_leverage_count(client.wallet_address)
active_id = None
for i in range(count, 0, -1):
pos = client.trading.get_leverage_position(client.wallet_address, i)
if pos[9]: # active flag
active_id = i
break
_, _, residual_tokens = main_lens.functions.getPartialLoanSellAmounts(MAIN, active_id, client.wallet_address, 50, True).call() # MainLens: caMAINLENS
quote = client.trading.get_amounts_out(residual_tokens, [MAIN, PAIR_ASSET])
sell_min = quote * 95 // 100 # no trade tax on the close; minOut is checked after the exit tax (at most 3%)
close = client.trading.partial_loan_sell_for(MAIN, active_id, 50, True, sell_min)
print("Partially closed:", close["hash"])
Cross-pair swap
<!-- MCP-PENDING: MCP.md §5 swap_cross, quote_cross -->SWAP.swapCross(path, amountIn, minOut, recipient, deadline, unwrapNative) swaps between tokens of different mains, and through outside pools where a route exists, in one transaction with one base fee. Quote first with SWAP.quoteCross(path, amountIn, trader): it is not a view, so call it with eth_call (simulate), never as a transaction; the quote includes the fees trader would pay. Approve path[0] to SWAP, or send the native coin with path[0] = the wrapped native token and msg.value == amountIn. deadline is a unix time. recipient must not be the zero address. A hop between two different pair assets, or through an outside token, needs a route in the hop executor; it is available only where hopExecutor in /contracts.json is not the zero address and such a route exists.
| Error | When |
|---|---|
"Deadline" | The transaction landed after deadline |
"no executor" | The route needs an outside hop and no hop executor is set |
"native in" | Native coin sent, but path[0] is not the wrapped native token or amountIn != msg.value |
"Transfer in" | path[0] could not be pulled from the wallet (approval or balance) |
"Min out" | The output is below minOut |
"no recipient" | recipient is the zero address |
9. Error Reference
| Error | Trigger | Fix |
|---|---|---|
"Min out" | A buy, sell or leverage buy returns less than minOut (for a leverage buy: the whole position) | Quote again on the taxed amount, lower the size or widen the tolerance |
"Min out not met" | A partial loan sell returns less than minOut | Quote the residual with getPartialLoanSellAmounts (§3d) |
"Zero bought" / "Zero sold" | The amount is too small to produce any output | Raise the amount |
"Under 2x" | leverageBuy() would not reach more than twice a plain buy | Simulate first; raise the input |
"Extend first" | A leverage buy that adds to your open position on the token, with fewer than minDaysLoan() days left on it | Extend the position first |
"Repay in full" | A part repay of a leverage position would leave less than minimumLoan() of debt | Repay more, or all |
"wrong path" | Leverage or mixed buy: path[0] is not the pair asset of the main in path[1], or the path is not 2 or 3 addresses | Read SWAP.pairs(main).pairAsset |
"Bad path" | The path is not 2 or 3 addresses | [pairAsset, main] or [pairAsset, main, token] |
"Bad pair" | The path does not match the token's own pair (for example [pairAsset, token]) | Rebuild the path from the token's main (token.quoteToken()) |
"Not main" / "Not MAIN" | The main in the path is not a registered main | Read SWAP.pairs(main) |
"Not pair asset" / "Wrong main" | The pair asset or the main in the path does not belong to the token's pair | Build the path from token.quoteToken() and pairs(main).pairAsset |
"unknown pair" | The main's pair is unknown or paused | Read SWAP.pairs(main).active |
"Alien token" / "Alien output" / "Wrong token" | The input or output is not a BASIS token that SWAP accepts | Check FACTORY.isEcosystemToken(token) |
"use swapCross" | sellTokens(…, swapToETH = true) | Native out goes through swapCross |
"In presale" | Leverage buy of an auto-vest token before its presale is complete | Wait for presaleComplete() |
"No WL for freeze" / "Overcapped on WL buying" / "WL cap" | Frozen token: the wallet is not whitelisted, or the buy passes its token cap | Ask the creator |
"% by 10" / "% range" | Partial close percentage not a multiple of 10, or outside 10 to 100 | Use 10, 20 … 100 |
"Not user" | Partial close of a position the caller does not own, or isLeverage = false from a wallet | Use your own leverage id with isLeverage = true; hub loans use LOANS.hubPartialLoanSell |
"Trading is not enabled" | The main's one-way launch switch is still shut | Read main.tradingEnabled() |
"has not bonded yet" / "Presale over" | Only on a direct call to the token in the wrong phase; SWAP picks the presale or the normal function itself | Trade through SWAP.buyTokens / sellTokens |
There is nothing to recover between the presale and open trading: SWAP routes a trade to the presale curve or the AMM by itself.
10. Strategy Context
| Situation | Recommended Action | Module |
|---|---|---|
| Bought STASIS | Stake it in its vault for rewards | →06 |
| Holding a vault stake | Borrow the pair asset against it, redeploy | →06 |
| Bought Floor+ token | Borrow against it (valued at the floor) | →05 |
| Want amplified exposure | Simulate then leverageBuy() (terminal path) | This module |
| Leveraged, want to take profit | partialLoanSellFor(main, …, isLeverage=true) in 10% increments | This module |
| Loan approaching expiry | Extend cheaply (400x less than re-originate) | →05 |
| Full multi-step capital stack | Build paths A→B→C→E for maximum efficiency | →12 |
The core insight from stacking strategies: Buying a token does not lock your capital. Borrow against almost any position to get the pair asset back, then deploy it again. Three stacked paths is the sweet spot: beyond that, fees erode the bag meaningfully. Leverage (Path E) is always the terminal step: it fully deploys your remaining pair asset and does not return capital for further stacking.
See Also
- Module 05: Lending: Borrow against any position you bought here
- Module 06: Staking: Stake a main in its vault for rewards
- Module 11: Token Mechanics: hybrid multiplier, floor math, presale internals
- Module 12: Strategy & Stacking: Chain buy → stake → borrow → leverage for full efficiency
- Module 18: SDK Reference: Full method signatures and error index
- Module 19: FAQ: Conversational answers on trading, slippage, leverage