# 0xSlots
> Collectively Owned Slots Protocol
## Contracts
### Addresses
#### Base Mainnet (8453)
| Contract | Address |
| ----------------- | -------------------------------------------- |
| SlotFactory (Hub) | `0xbf2F890E8F5CCCB3A1D7c5030dBC1843B9E36B0e` |
| MetadataModule | `0x0896A90c75d2cF195427fb4128f3f4b28f5b2Ef7` |
#### Base Sepolia (84532)
| Contract | Address |
| ----------------- | -------------------------------------------- |
| SlotFactory (Hub) | `0xc44De86e2A5f0C47f1Ba87C36DaBf54275814DEb` |
| MetadataModule | `0x65392ac6fd773a9bd36c200abf848c8fa3c9f7f8` |
| BatchCollector | `0xd3c7090C2F89c5132C3f91DD1da4bCffEAe10e13` |
| ERC721Slots | `0x65e88189ac09527c5F7da0296ef33C77E5a6BE27` |
#### Programmatic Access
```ts
import { slotFactoryAddress, getSlotsHubAddress, getSupportedChainIds } from "@0xslots/contracts";
slotFactoryAddress[8453]; // "0xbf2F890E..."
getSlotsHubAddress(8453); // Address | undefined
getSupportedChainIds(); // [8453, 84532]
```
***
### ABIs
```ts
import {
slotAbi, // Individual slot contract
slotFactoryAbi, // Factory / Hub contract
batchCollectorAbi, // BatchCollector utility
erc721SlotsAbi, // ERC721Slots (Harberger NFT collections)
metadataModuleAbi, // MetadataModule contract
} from "@0xslots/contracts";
```
#### Key Functions
| ABI | Function | Type | Description |
| ------------------- | ----------------------------- | ------------ | ------------------------ |
| `slotAbi` | `getSlotInfo` | Read | Full slot state |
| `slotAbi` | `buy` | Write | Buy or force-buy |
| `slotAbi` | `selfAssess` | Write | Update price |
| `slotAbi` | `topUp` / `withdraw` | Write | Manage deposit |
| `slotAbi` | `release` | Write | Release slot |
| `slotAbi` | `collect` | Write | Collect tax |
| `slotAbi` | `liquidate` | Write | Liquidate insolvent slot |
| `slotAbi` | `multicall` | Write | Batch calls |
| `slotFactoryAbi` | `createSlot` / `createSlots` | Write | Deploy slots |
| `metadataModuleAbi` | `updateMetadata` / `tokenURI` | Write / Read | Manage metadata |
## Getting Started
### Installation
:::code-group
```bash [pnpm]
pnpm add @0xslots/sdk @0xslots/contracts
```
```bash [npm]
npm install @0xslots/sdk @0xslots/contracts
```
:::
### Create a Client
```ts
import { SlotsClient, SlotsChain } from "@0xslots/sdk";
import { createPublicClient, createWalletClient, http } from "viem";
import { base } from "viem/chains";
const publicClient = createPublicClient({ chain: base, transport: http() });
// Read-only
const client = new SlotsClient({ chainId: SlotsChain.BASE, publicClient });
// Read + write
const walletClient = createWalletClient({
account: privateKeyToAccount("0x..."),
chain: base,
transport: http(),
});
const client = new SlotsClient({ chainId: SlotsChain.BASE, publicClient, walletClient });
```
### Read Slots
```ts
const { slots } = await client.getSlots({ first: 10 });
const { slot } = await client.getSlot({ id: "0x..." });
const info = await client.getSlotInfo("0x..."); // direct RPC read
```
### Write Actions
```ts
import { parseEther } from "viem";
// Buy a slot (ERC-20 approval handled automatically)
await client.buy({
slot: "0x...",
depositAmount: parseEther("1"),
selfAssessedPrice: parseEther("10"),
});
await client.selfAssess("0xSlot", parseEther("20"));
await client.topUp("0xSlot", parseEther("0.5"));
await client.withdraw("0xSlot", parseEther("0.1"));
await client.release("0xSlot");
await client.collect("0xSlot"); // permissionless
await client.liquidate("0xSlot"); // permissionless, earns bounty
```
### React Integration
```tsx
import { useSlotsClient, useSlotAction, useSlotOnChain } from "@0xslots/sdk/react";
function SlotView({ address }: { address: string }) {
const { data: slot, isLoading } = useSlotOnChain(address, 8453);
const { buy, busy } = useSlotAction({
onSuccess: (label, hash) => console.log(`${label}: ${hash}`),
});
if (isLoading) return
Loading...
;
if (!slot) return Not found
;
return (
Price: {slot.price.toString()}
Occupant: {slot.occupant ?? "Vacant"}
);
}
```
## Protocol
### Architecture
Every slot is a **standalone smart contract** — no shared state. Slots are deployed through a Factory using CREATE2 (ERC-1167 minimal proxy clones), giving deterministic addresses based on `keccak256(recipient, currency, config)`.
```
Factory
└─ createSlot(recipient, currency, config, initParams)
└─ CREATE2 → Slot Proxy (clone)
├─ immutable: recipient, currency, mutableTax, mutableModule, manager
└─ mutable: occupant, price, deposit, taxPercentage, module
```
#### Immutable Parameters (set at deployment)
| Parameter | Description |
| --------------- | ----------------------------------------------------------------- |
| `recipient` | Receives tax revenue. Can be EOA, multisig, Splits, DAO. |
| `currency` | ERC-20 token for pricing, deposits, and tax. |
| `mutableTax` | Whether tax rate can be changed by the manager. |
| `mutableModule` | Whether module can be changed by the manager. |
| `manager` | Can propose config updates. `address(0)` if both flags are false. |
#### Mutable State
| Field | Description |
| --------------- | ---------------------------------------------- |
| `occupant` | Current holder (zero address = vacant). |
| `price` | Self-assessed price (0 when vacant). |
| `deposit` | Escrowed ERC-20 for tax payments. |
| `taxPercentage` | Tax rate in basis points per month (100 = 1%). |
| `module` | Hook contract address. |
| `collectedTax` | Accumulated uncollected tax. |
***
### Slot Lifecycle
#### `buy(depositAmount, selfAssessedPrice)`
Settles tax on current occupant → refunds them → transfers price from buyer → stores deposit → sets new price. If vacant: buyer just deposits and self-assesses.
#### `release()`
Occupant exits. Settles tax → refunds deposit → slot becomes vacant.
#### `selfAssess(newPrice)` / `topUp(amount)` / `withdraw(amount)`
Occupant manages price and deposit. Cannot withdraw below `minDepositSeconds`.
#### `liquidate()`
Anyone can call when deposit is depleted. Slot returns to vacant, liquidator earns a bounty.
#### `collect()`
Permissionless. Sends accumulated tax to the `recipient`.
***
### Tax System
Tax accrues linearly: `taxOwed = price × taxPercentage × elapsed / (30 days × 10000)`
Tax is settled on every state change (`buy`, `release`, `selfAssess`, `topUp`, `withdraw`, `liquidate`).
The occupant maintains a deposit that covers future tax. When it's depleted, anyone can `liquidate()` and earn a bounty (`liquidationBountyBps`).
If `mutableTax` is true, the manager can propose rate changes via `proposeTaxUpdate()`. Changes only apply on the next ownership transition — the current occupant's terms are never changed under them.
***
### Roles
| Role | Revenue | Config | Slot State |
| ------------- | ---------------------------- | --------------------------- | --------------------------- |
| **Recipient** | Receives tax + sale proceeds | No control | No control |
| **Manager** | No revenue | Proposes tax/module changes | No control |
| **Occupant** | Pays tax | No control | Sets price, manages deposit |
No single address has "god-mode" admin powers. Recipient and manager can be the same address but don't have to be.
***
### Modules
Modules are optional hook contracts called on ownership transitions (`buy`, `release`, `liquidate`).
#### MetadataModule
The primary module. Lets the occupant attach a URI (metadata) to the slot.
```ts
await client.modules.metadata.updateMetadata(moduleAddress, slotAddress, "ipfs://...");
const uri = await client.modules.metadata.getURI(moduleAddress, slotAddress);
```
If `mutableModule` is true, the manager can propose module changes via `proposeModuleUpdate()`. Same pending-update model as tax changes.
:::warning
Modules execute arbitrary code during slot transitions. Only use verified modules or modules you've audited.
:::
## Subgraph
The subgraph indexes all slot deployments, ownership transitions, tax events, and metadata updates.
### Endpoints
| Network | Endpoint |
| ------------ | -------------------------------------------------------------------------------------------- |
| Base Mainnet | `https://gateway.thegraph.com/api/subgraphs/id/4sZrdv1SFzN4KzE9jiWDRuUyM4CnCrmvQ54Rv1s65qUq` |
| Base Sepolia | `https://gateway.thegraph.com/api/subgraphs/id/Z361DLoMdPh9WAopH7shJP8WoXYAB9XeKrLUCTYjdZR` |
### Key Entities
| Entity | Key Fields |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `Slot` | `id`, `recipient`, `currency`, `occupant`, `price`, `deposit`, `taxPercentage`, `collectedTax`, `totalCollected`, `createdAt`, `metadata` |
| `Account` | `id`, `type` (EOA/CONTRACT/DELEGATED/SPLIT), `slotCount`, `occupiedCount`, `slotsAsRecipient`, `slotsAsOccupant` |
| `Currency` | `id`, `name`, `symbol`, `decimals` |
| `Module` | `id`, `name`, `version`, `verified` |
| `MetadataSlot` | `id`, `uri`, `rawJson`, `adType`, `updatedBy`, `updateCount` |
### Event Entities
All events include `slot`, `timestamp`, `blockNumber`, and `tx`.
| Entity | Key Fields |
| ---------------------- | --------------------------------------------------------- |
| `BoughtEvent` | `buyer`, `previousOccupant`, `price`, `selfAssessedPrice` |
| `ReleasedEvent` | `occupant`, `refund` |
| `LiquidatedEvent` | `liquidator`, `occupant`, `bounty` |
| `SettledEvent` | `taxOwed`, `taxPaid`, `depositRemaining` |
| `TaxCollectedEvent` | `recipient`, `amount` |
| `PriceUpdatedEvent` | `oldPrice`, `newPrice` |
| `MetadataUpdatedEvent` | `author`, `uri`, `rawJson`, `adType` |
### Example Queries
#### List Slots
```graphql
{
slots(first: 10, orderBy: createdAt, orderDirection: desc) {
id
recipient
occupant
price
deposit
taxPercentage
currency { symbol decimals }
module { name verified }
createdAt
}
}
```
#### Slot Activity
```graphql
query GetSlotActivity($slot: String!) {
boughtEvents(where: { slot: $slot }, orderBy: timestamp, orderDirection: desc) {
buyer
price
selfAssessedPrice
timestamp
}
releasedEvents(where: { slot: $slot }, orderBy: timestamp, orderDirection: desc) {
occupant
refund
timestamp
}
liquidatedEvents(where: { slot: $slot }, orderBy: timestamp, orderDirection: desc) {
liquidator
bounty
timestamp
}
}
```
**SDK equivalent:** `await client.getSlotActivity({ slot: "0x..." })`
#### Account Lookup
```graphql
query GetAccount($id: ID!) {
account(id: $id) {
id
type
slotCount
occupiedCount
slotsAsRecipient { id price totalCollected }
slotsAsOccupant { id price deposit }
}
}
```
## SlotsClient
The main entry point for interacting with 0xSlots. [Source](https://github.com/nezz0746/0xSlots/blob/main/packages/sdk/src/client.ts)
### Constructor
```ts
import { SlotsClient, SlotsChain } from "@0xslots/sdk";
const client = new SlotsClient({
chainId: SlotsChain.BASE, // BASE (8453) or BASE_SEPOLIA (84532)
publicClient, // viem PublicClient
walletClient, // viem WalletClient (for writes)
subgraphApiKey, // optional — Bearer token
});
```
### Subgraph Queries
```ts
const { slots } = await client.getSlots({ first: 10, skip: 0 });
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 { modules } = await client.getModules({ first: 10 });
const activity = await client.getSlotActivity({ slot: "0x..." });
const events = await client.getRecentEvents({ first: 20 });
```
### RPC Reads
```ts
const info = await client.getSlotInfo("0x...");
// Returns: recipient, currency, occupant, price, deposit, taxPercentage,
// taxOwed, secondsUntilLiquidation, insolvent, pendingTax, pendingModule, ...
```
### Write Methods
All return `Promise`. ERC-20 approval is handled automatically (atomic via EIP-5792 when supported).
```ts
// Factory
await client.createSlot({ recipient, currency, config, initParams });
await client.createSlots({ ...params, count: 5n });
// Slot interactions
await client.buy({ slot, depositAmount, selfAssessedPrice });
await client.selfAssess(slot, newPrice);
await client.topUp(slot, amount);
await client.withdraw(slot, amount);
await client.release(slot);
await client.collect(slot);
await client.liquidate(slot);
// Manager
await client.proposeTaxUpdate(slot, newPct);
await client.proposeModuleUpdate(slot, newModule);
await client.cancelPendingUpdates(slot);
await client.setLiquidationBounty(slot, newBps);
// Multicall
await client.multicall(slot, [
{ functionName: "selfAssess", args: [newPrice] },
{ functionName: "topUp", args: [amount] },
]);
```
### Modules
```ts
await client.modules.metadata.getSlots({ first: 10 });
await client.modules.metadata.getURI(moduleAddress, slotAddress);
await client.modules.metadata.updateMetadata(moduleAddress, slotAddress, "ipfs://...");
```
## React Hooks
Wagmi-wired hooks from `@0xslots/sdk/react`. Requires `wagmi`, `viem`, and `@tanstack/react-query` as peer dependencies.
### `useSlotsClient`
Creates a memoized `SlotsClient` from wagmi's public and wallet clients.
```tsx
import { useSlotsClient } from "@0xslots/sdk/react";
const client = useSlotsClient(); // connected chain
const client = useSlotsClient(SlotsChain.BASE); // override chain
const client = useSlotsClient(SlotsChain.BASE, "api-key"); // with subgraph key
```
***
### `useSlotAction`
Unified write executor with transaction lifecycle tracking.
```tsx
import { useSlotAction } from "@0xslots/sdk/react";
const {
buy, selfAssess, topUp, withdraw, release, collect, liquidate,
createSlot, proposeTaxUpdate, proposeModuleUpdate, updateMetadata,
exec, // generic executor
busy, // isPending || isConfirming
isPending, // wallet interaction in progress
isConfirming, // waiting for on-chain confirmation
isSuccess,
activeAction, // label of current action
} = useSlotAction({
onSuccess: (label, hash) => console.log(`${label}: ${hash}`),
onError: (label, error) => console.error(`${label}: ${error}`),
});
```
***
### `useSlotOnChain`
Fetches a single slot's state from on-chain via RPC. Auto-invalidates on new blocks.
```tsx
import { useSlotOnChain } from "@0xslots/sdk/react";
const { data: slot, isLoading, refetch } = useSlotOnChain(address, 8453);
// slot: { occupant, price, deposit, taxPercentage, insolvent, currencySymbol, ... }
```
***
### `useSlotsOnChain`
Fetches multiple slots via multicall. Deduplicates currency metadata requests.
```tsx
import { useSlotsOnChain } from "@0xslots/sdk/react";
const { data: slots, isLoading } = useSlotsOnChain(addresses, 8453);
```