# 0xSlots > Collectively Owned Slots Protocol ## Contracts ### Addresses #### Base Mainnet (8453) | Contract | Address | | ----------------- | -------------------------------------------- | | SlotFactory (Hub) | `0xbf2F890E8F5CCCB3A1D7c5030dBC1843B9E36B0e` | #### Base Sepolia (84532) | Contract | Address | | ----------------- | -------------------------------------------- | | SlotFactory (Hub) | `0xc44De86e2A5f0C47f1Ba87C36DaBf54275814DEb` | *** ### ABIs ```ts import { slotAbi, // Individual slot contract slotFactoryAbi, // Factory / Hub contract metadataModuleAbi, // MetadataModule contract } from "@0xslots/contracts"; ``` ## Getting Started ### Installation :::code-group ```bash [pnpm] pnpm add @0xslots/sdk ``` ```bash [npm] npm install @0xslots/sdk ``` ::: ### 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"}

); } ``` import { Callout } from 'vocs/components' ## Overview 0xSlots is a **property primitive** for scarce onchain assets, based on a Harberger-tax-style model. It introduces slots: onchain positions that are **always priced** and **always contestable**. Each holder sets their own price and pays an ongoing fee based on that self-assessed value. But that price is not just informational, it is a standing sell offer. Anyone can buy the slot at that declared price, at any time. * Set the price too high, and the ongoing cost becomes expensive. * Set it too low, and someone takes it. * Set it fairly, and you keep control. This creates continuous pressure toward **fair** pricing and **productive** ownership. Passive property becomes costly to hold, while valuable positions tend to flow toward the people who value them most. **Revenue** can be directed to a creator, community, protocol, or public good, turning scarce digital property into an ongoing source of aligned revenue instead of idle control. *** ### How it works ![How 0xSlots works](/hand-drawn-diagram.jpg) Think of it like [Splits](https://splits.org) but for collective ownership. #### Slots Slots are smart contracts that are single use & immutable by default. * They fund **one recipient** * Use a **single currency** * At a **tax percentage** *(optionally mutable)* * Implement a **single module** *(optionally mutable)* If you want a different configuration, just deploy another. Slots hold a minimum deposit of currency (defined at launch by the user, protocol minimum: 1 day) that is the source of tax collections (which can be triggered by anyone). At creation you can also define a bounty % of the tax being collected to incentivize liquidations. #### Modules Modules are what can bring extra value to a slot. They're smart contracts that implement custom logic and are triggered by slot events (ownership transfer, price update, release). They can also require a cut of the tax revenue a slot generates through `feePercentage()` & `feeRecipient()`. ### Conclusion: Slots as Fertile Ground A slot starts empty. It's a position — nothing more. What makes it valuable is what the developpers do with it. Modules attached to a slot define the rights it grants: ad space, access, governance weight, revenue share, anything. **The slot is the plot; the module is the crop**. Each slot becomes a small, bounded space of productive ownership. Not a walled garden you lock people out of — a public garden you tend. The market decides if you're tending it well enough to keep it. This is the primitive. What grows on it is up to you. *** ### Quick Links * [Getting Started](/getting-started) — Install the SDK and create your first slot * [Protocol](/protocol) — Architecture, tax system, roles, and modules * [SDK Reference](/sdk/client) — `SlotsClient` API * [React Hooks](/sdk/react) — wagmi hooks for React apps * [Contracts](/contracts) — Addresses and ABIs * [Subgraph](/subgraph) — GraphQL queries ## Protocol ### SlotFactory The factory deploys slots via BeaconProxy (UUPS-upgradeable). Each slot gets a deterministic address based on `keccak256(recipient, currency, config)`. ```solidity function createSlot( address recipient, IERC20 currency, SlotConfig memory config, SlotInitParams memory initParams ) external returns (address slot); function createSlots( address recipient, IERC20 currency, SlotConfig memory config, SlotInitParams memory initParams, uint256 count ) external returns (address[] memory slots) ``` #### SlotConfig (immutable) ```solidity struct SlotConfig { bool mutableTax; // Can the tax rate be changed? bool mutableModule; // Can the module be changed? address manager; // Who can propose config changes (address(0) = no one) } ``` #### SlotInitParams ```solidity struct SlotInitParams { uint256 taxPercentage; // Tax rate in bps per month (100 = 1%) address module; // Hook contract (address(0) = none) uint256 liquidationBountyBps; // Bounty for liquidators in bps uint256 minDepositSeconds; // Minimum deposit to cover (protocol min: 1 day) } ``` The factory also maintains a **module registry** — modules can be verified so users know they're safe to use. *** ### Slot Each slot is a standalone smart contract. No shared state between slots. #### Core operations ```solidity /// Buy a slot (or take it from current occupant) function buy(address account, uint256 depositAmount, uint256 selfAssessedPrice) external; /// Leave the slot, get remaining deposit back function release() external; /// Change your self-assessed price function selfAssess(uint256 newPrice) external; /// Add to your deposit function topUp(uint256 amount) external; /// Withdraw excess deposit function withdraw(uint256 amount) external; /// Liquidate an insolvent slot (anyone can call, earns bounty) function liquidate() external; /// Send accumulated tax to the recipient (anyone can call) function collect() external; ``` #### Manager operations If `mutableTax` or `mutableModule` is true, the manager can propose changes. Updates only apply on the **next ownership transition** — the current occupant's terms never change under them. ```solidity function proposeTaxUpdate(uint256 newPct) external; function proposeModuleUpdate(address newModule) external; function cancelPendingUpdates() external; function setLiquidationBounty(uint256 newBps) external; ``` #### Tax Tax accrues linearly: `taxOwed = price * taxPercentage * elapsed / (30 days * 10000)` The occupant maintains a deposit that covers future tax. When depleted, anyone can `liquidate()` and earn a bounty. #### 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 | *** ### Modules Modules are optional hook contracts attached to a slot. They implement `ISlotsModule` (which extends `IERC165`): ```solidity interface ISlotsModule is IERC165 { // Identity function name() external view returns (string memory); function version() external view returns (string memory); // Lifecycle hooks (called by the Slot contract) function onTransfer(uint256 slotId, address from, address to) external; function onPriceUpdate(uint256 slotId, uint256 oldPrice, uint256 newPrice) external; function onRelease(uint256 slotId, address from) external; // Fee configuration function feeBps() external view returns (uint256); function feeRecipient() external view returns (address); // Metadata function moduleURI() external view returns (string memory); } ``` #### Lifecycle Hooks Called by the Slot contract during state transitions. `msg.sender` is always the slot contract. | Hook | Triggered by | Typical use | | ------------------------------------------- | -------------------------- | ------------------------------------- | | `onTransfer(slotId, from, to)` | `buy()` | Clear metadata, update access control | | `onPriceUpdate(slotId, oldPrice, newPrice)` | `selfAssess()` | React to price changes | | `onRelease(slotId, from)` | `release()`, `liquidate()` | Clean up slot state | #### Fee Configuration Modules can take a cut from collected tax. The fee is deducted when `collect()` is called on the slot. | Function | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | | `feeBps()` | Fee in basis points (e.g. `500` = 5%). Taken from collected tax before it reaches the `recipient`. Return `0` for no fee. | | `feeRecipient()` | Address that receives module fees — can be an EOA, multisig, Splits contract, etc. | #### Metadata | Function | Description | | ------------- | ------------------------------------------------------------------------------------------------------------ | | `moduleURI()` | URI pointing to module metadata (e.g. `ipfs://Qm...` with JSON containing image, description). Can be empty. | #### ERC-165 Modules must return `true` for both `ISlotsModule.interfaceId` and `IERC165.interfaceId` in `supportsInterface()`. The factory uses this to verify a contract is a valid module. #### MetadataModule The primary module. Lets the occupant attach a URI (e.g. IPFS) to the slot. Clears metadata on transfer and release. ```solidity // Set metadata for a slot (occupant only) function updateMetadata(address slot, string calldata uri) external; // Read metadata function tokenURI(address slot) external view returns (string memory); ``` #### FeedPostModule Like MetadataModule but supports trusted routers for atomic buy+post flows. Used by The Feed. ```solidity // Direct post (occupant only) function updateMetadata(address slot, string calldata uri) external; // Post via trusted router (atomic buy+post) function postFor(address account, address slot, string calldata uri) external; ``` :::warning Modules execute arbitrary code during slot transitions. Only use verified modules. ::: ## Subgraph The subgraph indexes all slot deployments, ownership transitions, tax events. ### 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 } } } ``` ## Vision ### Private Ownership Broke NFTs The NFT experiment revealed a fundamental flaw in how we handle digital property: **private ownership of scarce onchain assets creates toxic incentives**. What happened was predictable: * **Speculation replaced utility.** Prices inflated massively, driven by floor-sweeping and artificial scarcity, not by what the asset actually did. * **Crypto looked like a scam.** The bubble and its collapse drowned genuinely valuable projects in a sea of rugs and hype. The world watched and concluded the whole thing was stupid. * **Builders optimized for extraction.** Developers competed for attention instead of utility. Why build something useful when you can mint something scarce and pump it? The root cause wasn't the technology. It was the ownership model. When you own something absolutely - no cost to hold, no pressure to use - you're incentivized to hoard, speculate, and extract. The asset's *price* matters more than what it *produces*. ### A Middle Ground There is a real need for collectively owned assets that **stay in the market**. Not communal property. Not private property. Something in between: assets where ownership is continuous, contestable, and tied to productive use. 0xSlots implements this through Harberger taxation: you set your price, you pay a tax on it, and anyone can buy at that price. This shifts the focus: * **From what it's worth → to what it produces.** Holding idle property is expensive. Making it productive is how you justify the cost. * **From speculation → to fair valuation.** Self-assessed pricing with real cost creates continuous pressure toward honest pricing. No more arbitrary floors. No more diamond hands on dead assets. * **From extraction → to alignment.** Tax revenue flows to creators, communities, or public goods. The protocol turns ownership into a revenue stream for the ecosystem, not just the holder. This is the primitive. What grows on it is up to you. ## 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. ```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); ``` ## Adland Adland is an app built on top of slots that simply allows the current occupant of a slot to have write rights on the module's metadata IPFS URI string property. A specific open source registry ([`@adland/data`](#ad-type-registry-adlanddata)) handles standardization of data being uploaded & consumed by specialized components ([`@adland/react`](#react-components-adlandreact)). ### Module (onchain) #### MetadataModule The `MetadataModule` is the smart contract that powers Adland. It hooks into slot ownership events and stores a metadata URI (typically IPFS) per slot. ```solidity /// Set metadata for a slot (occupant only) function updateMetadata(address slot, string calldata uri) external onlyOccupant(slot); ``` When the occupant calls `updateMetadata`, the ad content (validated and enriched via `@adland/data`) is uploaded to IPFS and the resulting URI is stored on-chain. The module also listens to `onTransfer` and `onRelease` — when ownership changes, the previous occupant's metadata is cleared. *** ### Ad Type Registry (`@adland/data`) Each ad type defines a **data** schema (what the occupant provides) and a **metadata** schema (what gets auto-enriched). The pipeline runs: **parse** → **verify** → **enrich**. #### Ad Types ##### `link` ```ts data: { url: string } metadata: { title?, description?, image?, icon? } // Verified: URL format — Enriched: Open Graph tags ``` ##### `cast` ```ts data: { hash: string } metadata: { username?, pfpUrl?, text?, timestamp? } // Verified: cast exists on Neynar — Enriched: cast details ``` ##### `miniapp` ```ts data: { url: string } metadata: { icon?, title?, description?, imageUrl? } // Verified: .well-known/farcaster.json manifest — Enriched: manifest metadata ``` ##### `token` ```ts data: { address: string, chainId: number } metadata: { name?, symbol?, decimals?, logoURI? } // Verified: ERC-20 contract call — Enriched: token list / CoinGecko ``` ##### `farcasterProfile` ```ts data: { fid: string } metadata: { pfpUrl?, bio?, username?, displayName?, followers?, following?, pro? } // Verified: FID exists on Neynar — Enriched: profile data ``` #### API ```ts import { getAd, validateAdData } from "@adland/data"; // Get a definition const link = getAd("link"); // Validate link.data.safeParse({ url: "https://example.com" }); // Full pipeline: parse → verify → enrich const { data, metadata } = await link.process({ url: "https://example.com" }); // Safe version (no throw) const result = await link.safeProcess({ url: "https://example.com" }); // { success, data?, metadata?, error? } // Validate unknown ad data validateAdData({ type: "link", data: { url: "https://example.com" } }); ``` #### Custom Ad Types ```ts import { z } from "zod"; import { defineAd } from "@adland/data"; const myAd = defineAd({ type: "custom", data: z.object({ value: z.string() }), metadata: z.object({ enriched: z.string().optional() }), async verify({ value }) { /* throw if invalid */ }, async getMetadata({ value }) { return { enriched: "..." }; }, }); ``` *** ### React Components (`@adland/react`) Compound components for rendering ads. Fetches data from chain (RPC + IPFS) — no API keys required. #### Quick Start ```tsx import { Ad, AdImage, AdTitle, AdLoaded, AdEmpty, AdLoading } from "@adland/react"; Loading... Your ad here ``` #### `` Props There are two modes: **on-chain** (provide `slot` + `chainId`) or **static** (provide `data`). Use on-chain mode to fetch live ad content from the blockchain. Use static mode for previews or server-rendered content. | Prop | Required | Type | Default | Description | | ------------- | ------------------ | ----------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `slot` | **yes** (on-chain) | `string` | — | Slot contract address. Triggers on-chain fetch: metadata URI → IPFS content. | | `chainId` | **yes** (on-chain) | `SlotsChain` | — | Chain the slot is deployed on. | | `data` | **yes** (static) | `AdData` | — | Static ad data. Skips on-chain fetch entirely. | | `rpcUrl` | no | `string` | public RPC | Custom RPC endpoint. Use if the default public RPC is rate-limited or slow. | | `baseLinkUrl` | no | `string` | `https://app.0xslots.org` | Base URL for the empty-state CTA. When a slot has no ad, clicking opens `{baseLinkUrl}/slots/{slot}`. | | `auth` | no | `"farcaster" \| "none"` | `"none"` | Auth method for analytics. `"farcaster"` uses Quick Auth to attach a verified FID to impression/click events. | | `context` | no | `string` | — | Placement label sent with analytics events (e.g. `"sidebar"`, `"carousel"`, `"frame"`). Useful for filtering by placement. | :::info Provide either `slot` + `chainId` **or** `data` — not both. If `data` is provided alongside `slot`, the static data is used and no fetch occurs. ::: #### Content Components Render inside ``. Each accepts standard HTML props. | Component | Renders | Extra props | | ----------------- | ---------------------------- | ----------- | | `` | Ad image (``) | `fallback` | | `` | Ad title (`

`) | `fallback` | | `` | Ad description (`

`) | `fallback` | | `` | Type icon + label (``) | — | | `` | Static "AD" label (``) | — | #### State Components | Component | Renders when | | ------------- | ----------------- | | `` | Ad data available | | `` | Slot has no ad | | `` | Fetch in progress | | `` | Fetch failed | #### `useAd()` Hook ```tsx import { useAd } from "@adland/react"; const { data, cid, isLoading, error, isEmpty, slot, chainId } = useAd(); ``` #### Click Behavior | State | Action | | ---------- | ----------------------------------------------------- | | **Loaded** | Performs ad action (open link, view cast, etc.) | | **Empty** | Opens `${baseLinkUrl}/slots/${slot}?chain=${chainId}` | In a Farcaster miniapp, actions use the native SDK. On web, falls back to `window.open`. #### Full Example ```tsx

Your ad here
```