SlotsClient
The main entry point for interacting with 0xSlots. Source
Constructor
import { SlotsClient, SlotsChain } from "@0xslots/sdk";
const client = new SlotsClient({
chainId: SlotsChain.BASE, // BASE (8453), BASE_SEPOLIA (84532), ANVIL (31337)
publicClient, // viem PublicClient
walletClient, // viem WalletClient (for writes)
apiUrl, // optional — defaults to DEFAULT_API_URL
headers, // optional — extra request headers
});Endpoints
import { DEFAULT_API_URL, LOCAL_API_URL } from "@0xslots/sdk";DEFAULT_API_URL is the hosted indexer, used when apiUrl is omitted.
LOCAL_API_URL (http://localhost:42069/graphql) is what pnpm dev:local
starts, for chain 31337.
There is no apiKey
The indexer serves GraphQL unauthenticated, so a key bought nothing — and
consumers wired it through NEXT_PUBLIC_*, which Next inlines into the client
bundle. The option was removed rather than ignored. If you have put your own
deployment behind auth, headers carries it and is now the only path:
new SlotsClient({
chainId,
apiUrl,
headers: { Authorization: `Bearer ${token}` },
});Indexer queries
const { slots } = await client.getSlots({ limit: 10 });
const { slot } = await client.getSlot({ id: "0x..." });
const { slots } = await client.getSlotsByRecipient({ recipient: "0x..." });
const { slots } = await client.getSlotsByOccupant({ occupant: "0x..." });
const { factory } = await client.getFactory();
const activity = await client.getSlotActivity({ slot: "0x..." });
const events = await client.getRecentEvents({ limit: 20 });Result shape
Lists return { items, totalCount, pageInfo } — not a bare array. Callers that
iterated a response directly need .items; the failure mode is
object is not iterable at the call site.
const { items, totalCount, pageInfo } = (await client.getSlots({ limit: 10 })).slots;Pagination is limit with either offset or the after / before cursors from
pageInfo. The subgraph's first / skip are gone, as is its block:
time-travel argument, which has no equivalent.
Accounts
getAccounts returns protocol-wide totals; getAccountChains returns the same
counters scoped to one chain. Reach for the second whenever you are showing a
per-chain view — an account has no chainId, so the unscoped list will happily
show one chain's recipients with another chain's counts.
const { accounts } = await client.getAccounts({ limit: 20 });
const { accountChains } = await client.getAccountChains({ limit: 20 });
// accountChain: { slotCount, occupiedCount, occupiedAsRecipient }occupiedAsRecipient is the only honest numerator for an occupancy percentage:
occupiedCount counts slots the account occupies, slotCount counts slots
where it is the recipient. They describe different roles, and pairing them as a
ratio is a category error however much they look like a pair.
Indexing status
const { _meta } = await client.getMeta(); // { status: Record<string, ChainStatus> }There is no hasIndexingErrors counterpart: the indexer stops rather than
serving stale rows behind a flag, so an erroring indexer is a failed request, not
a true here.
RPC reads
const info = await client.getSlotInfo("0x...");
// terms (recipient, currency, taxBps, mutableTax, mutableHook), the
// attached hook and its snapshotted flags, live occupancy and financials
// (occupant, price, deposit, taxOwed, secondsUntilLiquidation, isInsolvent),
// and any queued terms (pendingTaxBps, pendingHook, hasRipeTerms).
const infos = await client.getSlotsInfo(["0x...", "0x..."]); // multicall
const p = await client.pending("0x..."); // just the queued terms
const s = await client.slotState("0x..."); // just the live occupancyWrite methods
All return Promise<Hash>. ERC-20 approval is handled automatically.
// Create — one struct, no batch variant
await client.createSlot({
recipient, currency, manager, hook, // hook: a hook address, or ZERO for none
taxBps: 100n, minDepositSeconds: 0n,
mutableTax: true, mutableHook: false,
});
// Occupant interactions
await client.buy({ slot, account, selfAssessedPrice, depositAmount /*, maxPayment */ });
await client.selfAssess(slot, newPrice);
await client.topUp(slot, amount);
await client.withdraw(slot, amount);
await client.release(slot);
await client.collect(slot); // permissionless
await client.liquidate(slot); // permissionless — no bounty
await client.claim(slot); // pull a credited refundbuy takes an account — who becomes occupant. It need not be the signer, so
you can buy on someone's behalf. The SDK takes named params, but note the
on-chain order is (account, selfAssessedPrice, depositAmount, maxPayment) —
price before deposit, matching every other pair in the protocol. Pass maxPayment to cap what an ERC-20 buy can
cost against the occupant raising the price between your quote and your inclusion.
SlotInit
The create tuple is a single SlotInit. viem encodes a struct by component
name, so the SDK exposes a typed shape — a missing or stray field is a compile
error rather than a silent zero address on-chain.
type SlotInit = {
recipient: Address; currency: Address; // currency ZERO = native ETH
manager: Address; hook: Address; // both ZERO for immutable, hookless
taxBps: bigint; minDepositSeconds: bigint;
mutableTax: boolean; mutableHook: boolean;
};Manager operations
Tax and hook are the two mutable dimensions, proposed and cancelled independently. Proposed terms apply after a one-day delay.
await client.proposeTerms(slot, { taxBps: 200n }); // just the tax
await client.proposeTerms(slot, { hook: newHook }); // just the hook
await client.proposeTerms(slot, { taxBps: 200n, hook: newHook });
await client.proposeTerms(slot, { hook: ZERO_ADDRESS }); // detach the hook
await client.cancelTerms(slot, true, false); // just the tax
await client.cancelTerms(slot); // both (cancelTax/cancelHook default true)Selling into an offer
client.sell, makeSellOrder and cancelSellOrder are gone with Slot.sell.
Nothing is signed off-chain any more: a bid is an on-chain row in the OfferBook,
and the book performs the sale itself.
// as the bidder — one transaction, funds stay in your wallet
await book.offer(slot, price, deposit, expiry);
// as the occupant — grant once per tenure, then accept
await client.setOperator(slot, book, true);
await book.acceptOffer(slot, id);acceptOffer reprices the slot to the bid and seats the bidder in one
transaction, which is why the book needs the operator grant. That grant is keyed
by tenure on the slot's side, so it lapses when the slot changes hands and cannot
be inherited by the next occupant.
The bidder approves the book for price + deposit, not the slot: buy
charges msg.sender, and on a fill that is the book.
Configuring a hook
A hook's per-slot settings live on the SLOT, as hookData — 32 bytes the slot
stores at creation and hands back on every callback. That is what lets one
deployment serve every configuration: a seven-day minimum tenure and a thirty-day
one point at the same contract.
import { minimumTenureHookAddress } from "@0xslots/contracts/slots";
import { toHex } from "viem";
await client.createSlot({
// …
hook: minimumTenureHookAddress[chainId],
hookData: toHex(7n * 24n * 3600n, { size: 32 }), // the window, in seconds
});There used to be a predictTenureHook / getOrDeployTenureHook pair here,
because a duration meant a whole contract at a derived address — an unusual
number cost a second transaction. It costs nothing now, and those functions are
gone.
hookData travels with hook and only with it. Passing data alongside no hook
is refused, and so is data the hook itself rejects — a MinimumTenureHook given
a zero window says so at creation rather than vetoing every buy afterwards.
await client.proposeTerms(slot, {
hook: minimumTenureHookAddress[chainId],
hookData: toHex(30n * 24n * 3600n, { size: 32 }),
});A slot wanting more than one behaviour points at one hook that implements all of
them. That hook owns the whole 32 bytes and may pack several fields into a
single word — the layout is its own, and validateHookData is where it says so.
Multicall
await client.multicall(slot, [
{ functionName: "selfAssess", args: [newPrice] },
{ functionName: "topUp", args: [amount] },
]);