Skip to content
0xSlots

SlotsClient

The main entry point for reading and writing slots. It talks to the contracts over RPC — it does not query the indexer. Source

Constructor

import { SlotsClient } from "@0xslots/sdk";
 
const client = new SlotsClient({
  publicClient,      // viem PublicClient — every read
  walletClient,      // viem WalletClient — every write; needs an account and a chain
  factoryAddress,    // SlotFactory — only createSlot and the collectAll family
  offerBookAddress,  // OfferBook — defaults to the one recorded for the wallet's chain
  lensAddress,       // SlotLens — slotState, slotStates, moduleUpdate; same default
});

Every field is optional; a method that needs a missing one throws a SlotsError naming it. createSlotsClient(config) is the same thing as a function.

Reads

const state = await client.slotState(slot);
// occupant, price, deposit, taxOwed, isVacant, isInsolvent,
// secondsUntilLiquidation, currency, taxRateBps, minRunwaySeconds, recipient,
// manager, mutableTax, mutableRecipient, mutableModule, module, settings,
// fee, scopes, pending, occupiedSince, lastSettled, collectedTax, tenureId

slotState is one SlotLens.getSlotInfo call, and slotStates(slots) reads many in one call. Individual reads exist for the common fields (occupant, price, deposit, taxOwed, module, scopes, fee, terms, tenureId, …), plus:

MethodReturns
quoteBuy(slot, account, deposit)what buy will charge, including account's debt
minDepositForBuy(slot, price)the smallest deposit a buy accepts, under ripe queued terms
minDepositToHold(slot, price)the floor selfAssess and withdraw enforce
pending(slot)queued terms, with appliesAt and applies
hasRipeTerms(slot)whether the next buy lands them — the chain's clock, not yours
fee(slot) / scopes(slot)the module's fee and scopes, as the slot copied them
moduleUpdate(slot){ current, declared, feeDiffers, scopesDiffer }, from the lens
debtOf(slot, account) / claimableOf(slot, account?)debt carried / credits to claim
isOperator(slot, operator)live, per tenure

Writes

All return Promise<Hash>. ERC-20 approval is handled automatically.

await client.createSlot(init);                 // see SlotInit below
await client.simulateCreateSlot(init);         // the address it would get
 
await client.buy({ slot, account, selfAssessedPrice, depositAmount /*, maxPayment */ });
await client.simulateBuy(params);              // throws the module's own error on refusal
await client.liquidateAndBuy(params);          // ERC-20 slots, one multicall
 
await client.selfAssess(slot, newPrice);
await client.topUp(slot, amount);
await client.withdraw(slot, amount);
await client.manageTerms(slot, { newPrice, topUpAmount, withdrawAmount });
await client.setOperator(slot, operator, true);
await client.release(slot);
 
await client.collect(slot);                    // permissionless
await client.collectAll(slots);                // through the factory
await client.simulateCollectAll(slots);        // what each would pay out
await client.liquidate(slot);                  // permissionless — no bounty
await client.claim(slot, account);             // pull a credited payout

buy takes an account — who becomes occupant; it need not be the signer. It sends the quote it just read as maxPayment unless you pass one, so the transaction pays what it was quoted or reverts. maxPayment: 0n disables the ceiling on purpose.

manageTerms reprices and moves the deposit as one submission, in the only order that passes both funding checks: top up, reprice, withdraw. A native top-up goes first as its own transaction, because multicall is not payable.

SlotInit

import { zeroAddress } from "viem";
 
await client.createSlot({
  currency: zeroAddress,           // native ETH
  manager: "0x...",                // required exactly when something is mutable
  mutableTax: true,
  mutableRecipient: false,
  mutableModule: true,
  taxTerms: { recipient: "0x...", rateBps: 100, minRunwaySeconds: 86_400 },
  moduleTerms: { module, settings }, // omit for no module
});

viem encodes a struct by component name, so the SDK takes a typed shape and assertSlotInit refuses what the contract would refuse before any gas is spent.

Manager operations

await client.proposeTerms(slot, { taxRateBps: 200 });
await client.proposeTerms(slot, { recipient, minRunwaySeconds: 3_600 });
await client.proposeTerms(slot, { moduleTerms: { module, settings } });
await client.proposeTerms(slot, { moduleTerms: NO_MODULE });   // remove the module
 
await client.cancelTerms(slot, TERMS.MODULE);  // one term
await client.cancelTerms(slot);                // everything (ALL_TERMS)
 
const update = await client.moduleUpdate(slot);
if (update.feeDiffers) await client.acceptFee(slot, update.declared!.fee);          // now
if (update.scopesDiffer) await client.acceptScopes(slot, update.declared!.scopes);  // next buy
 
await client.setManager(slot, next);

Presence, not truthiness, decides what proposeTerms queues. Terms land at the next buy after the one-hour delay — see How a slot works.

Modules

import { minimumTenureModuleAddress } from "@0xslots/contracts";
import { toHex } from "viem";
 
const module = minimumTenureModuleAddress[chainId];
const settings = toHex(7n * 24n * 3600n, { size: 32 });  // abi.encode(uint256 window)
 
await client.validateSettings(module, settings); // { ok: true } | { ok: false, reason }
await client.readScopes(module, settings);       // what the module asks, today
await client.readFee(module, settings);
await client.moduleMetadata(module);             // parsed metadata() JSON, or null

validateSettings runs the same check the slot runs at proposal and resolves with the module's own reason rather than throwing, so a form can show it.

Constants: TERMS, ALL_TERMS, SCOPE_BITS, unpackScopes, NO_MODULE, NO_SETTINGS, MAX_PRICE, MAX_TAX_BPS, BASIS_POINTS, MONTH_SECONDS, TERMS_DELAY_SECONDS.

Selling through the OfferBook

// as the bidder — posting moves nothing; approve the book first
await client.approveOfferBook(slot, await client.offerCost(slot, me, price, deposit));
await client.postOffer({ slot, price, deposit, expiry });
 
// as the occupant — grant once per tenure, then accept
await client.authorizeOfferBook(slot);
const { best } = await client.offerBoard(slot);
if (best) await client.acceptOffer(slot, best.id, best.price);

offerBoard returns only live offers, highest first, as the book itself judges them. See OfferBook.

Collections and collectives

Two more clients ship beside SlotsClient:

  • CollectionsClient creates and mints slot-bound NFT collections (createCollection, quoteMint, approveMint, mint, setBaseURI).
  • CollectivesClient creates and governs SlotCollectives — a split recipient with role-gated management. COLLECTIVE_ROLES names the roles: tax proposes tax rates, policy proposes modules and accepts their updates (proposeModule, acceptFee, acceptScopes, cancelModuleProposal), split edits the payout. Holders are set at creation with taxManagers, policyManagers and splitManagers.