Skip to content
0xSlots

Indexer

Slot deployments, ownership transitions, tax and module events are indexed by Ponder (packages/ponder) and served as GraphQL. SlotsClient itself reads the chain; the indexer is for lists and history.

Endpoint

Hostedhttps://0xslots-production.up.railway.app/graphql (development instance: https://0xslots-dev.up.railway.app/graphql)
Localhttp://localhost:42069/graphql (pnpm dev:local)

@0xslots/sdk exports them as DEFAULT_API_URL, LOCAL_API_URL, API_URLS and apiUrlFor(env). The API is unauthenticated.

Shape

  • Plural fields return { items, totalCount, pageInfo }, not a bare list.
  • Pagination is limit with offset, or the after / before cursors from pageInfo.
  • Foreign keys are scalar columns; the joined row is the *Ref sibling — module is an address, moduleRef is the row.
  • There is no block: argument. Ponder has no time-travel query.

Key entities

EntityKey fields
slotid, chainId, recipient, currency, manager, occupant, isOccupied, price, deposit, taxRateBps, minRunwaySeconds, module, settings, moduleFeeBps, moduleFeeRecipient, the scope* flags, collectedTax, taxPaidTotal, totalCollected, moduleFeesTotal, tenureId, lastSettled, createdAt
moduleid, chainId, declaredKnown and the declared* flags, slotCount, failedCallCount
accountid, type (EOA/CONTRACT/DELEGATED/SPLIT), slotCount, occupiedCount, totalHoldTime, taxPaidTotal
accountChainaccount, chainId, slotCount, occupiedCount, occupiedAsRecipient
currencyid, chainId, name, symbol, decimals

A slot points at one module (or none). Scopes are stored on both rows and they answer different questions: slot.scope* is the copy the slot accepted — what it obeys — while module.declared* is what the module declared when the indexer first saw it. Comparing them is how you find a module that changed its scopes after slots had committed to it.

slot.scopeAfterCallbacksMustSucceed is the one worth surfacing: such a module's after callbacks run uncapped and can fail the slot, eviction included.

module.failedCallCount counts after callbacks that reverted and were swallowed (moduleCallFailedEvent). A module accumulating those is quietly broken.

slot also carries its queued terms: pendingMask and, per term, pendingHasTaxRate / pendingTaxRateBps, pendingHasRecipient / pendingRecipient, pendingHasMinRunway / pendingMinRunwaySeconds, pendingHasModule / pendingModule / pendingModuleSettings, pendingHasScopes / pendingScopes, and pendingProposedAt.

account vs accountChain

account has no chainId and holds protocol-wide totals. accountChain holds the same counters per chain, and is what a per-chain view must read.

occupiedCount counts slots the account occupies; slotCount counts slots where it is the recipient. The one that pairs with slotCount is occupiedAsRecipient — the only honest numerator for an occupancy percentage.

Schema drift

The indexer serves exactly one schema, and GraphQL rejects a document containing an unknown field — so a client querying a renamed column gets an error or an empty list, not a missing field. Deploy the indexer and its consumers together.

Example queries

List slots

{
  slots(
    where: { chainId: 31337 }
    orderBy: "createdAt"
    orderDirection: "desc"
    limit: 10
  ) {
    items {
      id
      recipient
      occupant
      price
      deposit
      taxRateBps
      module
      currencyRef { symbol decimals }
      moduleRef { slotCount failedCallCount }
    }
    totalCount
    pageInfo { hasNextPage endCursor }
  }
}

Slot activity

query GetSlotActivity($slot: String!) {
  boughtEvents(where: { slot: $slot }, orderBy: "timestamp", orderDirection: "desc") {
    items { buyer from price deposit paid tenure timestamp }
  }
  releasedEvents(where: { slot: $slot }, orderBy: "timestamp", orderDirection: "desc") {
    items { occupant refund timestamp }
  }
  liquidatedEvents(where: { slot: $slot }, orderBy: "timestamp", orderDirection: "desc") {
    items { by occupant heldFor timestamp }
  }
}

Module money and failures have their own tables: moduleFeePaidEvents and moduleCallFailedEvents, both keyed by slot and module.

Indexing status

{ _meta { status } }

status is keyed by chain and carries the latest indexed block. An erroring indexer stops rather than serving stale rows, so it shows up as a failed request.

Creatives

One module is indexed: AdLand, whose creative is the entire content of a sponsor space and lives nowhere else.

EntityKey fields
creativeslot, chainId, module, uri, tenureId, publisher, clearedAt, publishCount, firstPublishedAt
publishedEventid, slot, module, uri, tenureId, publisher, timestamp, tx
clearedEventid, slot, module, fromTenure, toTenure, timestamp, tx
adKeykey, chainId, module, slot, pendingSlot, pendingReadyAt, setCount

Two events, and both matter. Published is somebody putting a creative up. Cleared is the module blanking one because the slot changed hands — emitted from afterBuy, afterRelease and afterLiquidate. Reading only the first leaves a creative in the index long after the chain has stopped serving it.

A clear is recorded, not applied: the row keeps its uri and gains a clearedAt, mirroring the contract, which refuses to return a creative once tenureId has moved on.