Skip to content
0xSlots

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 occupancy

Write 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 refund

buy 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] },
]);