Documentation

Scallar Docs

Scallar is an open source indexer for Stock Tokens on Robinhood Chain. It tracks every ERC-8056 multiplier change at the exact block it landed, so balances and position history read the way a brokerage statement reads.

ChainRobinhood Chain
Chain ID4663
Base URLhttps://api.scallar.finance
StandardERC-8056
API live

The REST API is live at https://api.scallar.finance and every request and response body on this page was captured from it. The indexer is still backfilling full chain history, so some tokens report partial data until that completes. The GraphQL endpoint and the @scallarhq/sdk package are planned and not yet published; their sections below show previews only.

Introduction

Stock Tokens on Robinhood Chain implement ERC-8056. Alongside the usual ERC-20 surface they expose uiMultiplier(), a display scalar that the issuer moves when a corporate action happens. When SGOV processed the first corporate action on the chain at block 4,629,631, no tokens were minted and no balance was rewritten. The multiplier moved from 1.0 to 1.000957519890990718 and every holder’s display balance grew by 0.0958 percent.

A generic EVM indexer never learns this. It reads balanceOf(), sums Transfer logs, and stops. The raw number it reports is technically correct and quietly wrong as a position: it stays frozen while the true share count drifts away from it, and every downstream valuation inherits the error.

Worked example: SGOV multiplier update at block 4,629,631

ReadingGeneric EVM indexerScallar
rawBalance100.000000000000000000100.000000000000000000
uiMultiplier before block 4,629,631not read1.0
uiMultiplier after block 4,629,631not read1.000957519890990718
uiBalance reported100.000000000000000000100.095751989099071800
Change attributednone, the event is invisible+0.0957519890990718%

What Scallar gives you

  • Full multiplier history per token, keyed to the exact block and transaction hash where the change landed.
  • A corrected uiBalance next to the raw balance on every position and holder row, so you can audit the arithmetic yourself.
  • A live REST API at https://api.scallar.finance and a public explorer over the same data.
  • Free API keys from a single curl call, with higher tiers unlocked by staking SCALLAR.

Quickstart

Two curl commands take you from nothing to a correct token read. There is no SDK to install; the REST API is the product.

1. Create a free key

bash
curl -X POST https://api.scallar.finance/v1/keys \
  -H "Content-Type: application/json" \
  -d '{"label": "my first key"}'

The API answers with a 201 and your key:

json
{
  "apiKey": "scl_ab12cd34ef56ab78cd90ef12ab34cd56ef78ab90",
  "keyPrefix": "scl_ab12cd",
  "tier": "free",
  "note": "store this key now, it will not be shown again"
}

Store the key immediately; it is never shown again. The key is also optional. Requests without one run on the Free tier keyed by IP address, at the same 60 requests per minute. See rate limits.

2. Call the API

bash
curl https://api.scallar.finance/v1/tokens/TSLA \
  -H "x-api-key: scl_ab12cd34ef56ab78cd90ef12ab34cd56ef78ab90"

3. Read the response

json
{
  "address": "0x322f0929c4625ed5bad873c95208d54e1c003b2d",
  "symbol": "TSLA",
  "name": "Tesla",
  "decimals": 18,
  "uiMultiplier": "1000000000000000000",
  "uiMultiplierFloat": 1,
  "totalSupply": "2738490000000000000000",
  "totalSupplyFormatted": "2738.490000000000000000",
  "totalSupplyUi": "2738490000000000000000",
  "holders": 8397,
  "priceUsd": 387.73,
  "lastSyncedBlock": "0"
}

Every amount appears in two forms. totalSupply is the raw integer the contract stores and totalSupplyFormatted is the same value divided by the token’s 18 decimals. uiMultiplier is the ERC-8056 scalar, 1.0 for TSLA because Tesla has had no corporate action on chain yet. lastSyncedBlock of "0" means the transfer backfill for this token has not completed; supply, holders, and multiplier data are live either way.

Core concepts

uiMultiplier()

uiMultiplier() returns an unsigned 18 decimal fixed point number. A value of 1000000000000000000 means 1.0 and leaves display balances equal to raw balances. It is a presentation scalar only. Changing it never mints, never burns, and never touches a single storage slot in the balance mapping, which is exactly why an indexer that only follows Transfer logs cannot see it happen.

solidity
// ERC-8056 display scalar, 18 decimal fixed point
function uiMultiplier() external view returns (uint256);

// Emitted when a corporate action rescales the display balance
event MultiplierUpdated(uint256 oldMultiplier, uint256 newMultiplier);

Raw balance versus UI balance

Every quantity Scallar returns carries an explicit basis. The conversion is one multiplication:

uiBalance = rawBalance × uiMultiplier / 1e18
js
uiBalance = rawBalance * uiMultiplier / 1e18

// SGOV after the multiplier update at block 4,629,631
// rawBalance    100000000000000000000  (100 SGOV, 18 decimals)
// uiMultiplier  1000957519890990718    (1.000957519890990718)
// uiBalance = 100 * 1.000957519890990718 = 100.095751989099071800
TokenrawBalanceuiMultiplieruiBalance
TSLA2.7985736304900060761.02.798573630490006076
SGOV100.0000000000000000001.000957519890990718100.095751989099071800

Exact amounts cross the wire as strings, because parsing "2738490000000000000000" into a JavaScript number loses precision above 2^53. Float convenience fields such as uiMultiplierFloat, priceUsd, and share exist for display. Do arithmetic on the string fields.

Corporate actions

Every multiplier change is stored with the old value, the new value, the block, the transaction hash, and the percentage change. So far Robinhood Chain has produced exactly one corporate action, the SGOV update shown below. Splits, reverse splits, and stock dividends move the same scalar, so when they happen they will appear in this same feed. This is the live response from GET /v1/tokens/SGOV/multipliers:

json
[
  {
    "blockNumber": "4629631",
    "txHash": "0x79292bc8af671bd6fc4ebc8f5e7a27c814d87988e175cbfab2ab1db1df0efbfb",
    "oldMultiplier": "1000000000000000000",
    "newMultiplier": "1000957519890990718",
    "oldMultiplierFloat": 1,
    "newMultiplierFloat": 1.0009575198909908,
    "effectiveAt": "1783541672",
    "changePct": 0.0957519890990718
  }
]

A multiplier above 1.0 grows display balances while raw balances hold still. A reverse split would push it below 1.0 and shrink them. Because every event is keyed to a block, history stays auditable: you can always recompute a wallet’s uiBalance at any point from the raw balance and the multiplier in force at that block.

REST API

All routes live under https://api.scallar.finance/v1 and return JSON. Authentication is a key in either the Authorization: Bearer header or the x-api-key header. Anonymous requests work on the Free tier, rate limited by IP address.

bash
curl https://api.scallar.finance/v1/tokens/TSLA \
  -H "Authorization: Bearer scl_YOUR_KEY"

# the x-api-key header is equivalent
curl https://api.scallar.finance/v1/tokens/TSLA \
  -H "x-api-key: scl_YOUR_KEY"

Endpoints

MethodPathDescription
GET/v1/statusIndexer snapshot: token count, how many are backfilled, and the last indexed block range.
GET/v1/tokensEvery indexed Stock Token with multiplier, supply, holder count, and price, ordered by holders descending.
GET/v1/tokens/{id}One token by ticker or 0x address, plus its lastSyncedBlock.
GET/v1/tokens/{id}/multipliersMultiplier history for a token, newest first: old value, new value, block, transaction hash, change percent.
GET/v1/tokens/{id}/holdersTop holders for a token with raw, formatted, and multiplier corrected balances plus supply share.
GET/v1/balances/{address}Every position held by an address, with raw, formatted, and UI balances plus USD value.
GET/v1/balances/{address}/historyTransfer history for an address, newest first. Filter by ticker, cap with limit.
POST/v1/keysCreate a free API key. Body accepts optional label and walletAddress. Rate limited by IP.
GET/v1/keys/meInspect the calling key: prefix, tier, limits, and usage today. Requires auth.
POST/v1/keys/refreshRecompute the key tier from staked SCALLAR once the staking contract is configured. Requires auth.

Query parameters

ParameterTypeApplies toDescription
limitintegerholders, historyMaximum number of rows returned, for example ?limit=50.
tickerstringhistoryRestrict the event stream to one Stock Token, for example NVDA.

Example: GET /v1/status

json
{
  "tokens": 93,
  "backfilled": 23,
  "minLastIndexed": "0",
  "maxLastIndexed": "14821371",
  "updatedAt": "2026-07-20 15:49:23.213627+00",
  "note": "DB-only snapshot; block figures are the last indexed block, not the chain head"
}

The block figures are the last blocks the indexer has written, not the chain head. While the backfill runs, backfilled counts the tokens whose full transfer history is already complete.

Example: GET /v1/tokens

Returns an array ordered by holder count descending, 93 tokens at the time of capture. The first element:

json
[
  {
    "address": "0x322f0929c4625ed5bad873c95208d54e1c003b2d",
    "symbol": "TSLA",
    "name": "Tesla",
    "decimals": 18,
    "uiMultiplier": "1000000000000000000",
    "uiMultiplierFloat": 1,
    "totalSupply": "2738490000000000000000",
    "totalSupplyFormatted": "2738.490000000000000000",
    "totalSupplyUi": "2738490000000000000000",
    "holders": 8397,
    "priceUsd": 387.73
  }
]

Example: GET /v1/tokens/TSLA/holders?limit=50

json
[
  {
    "holder": "0x2f4579ca81717d3d61bf8b6f06571877bbe54a07",
    "rawBalance": "745903681766298514678",
    "balance": "745.903681766298514678",
    "uiBalance": "745.903681766298514678",
    "share": 27.23777270562604
  }
]

Example: GET /v1/balances/{address}

json
{
  "address": "0x2f4579ca81717d3d61bf8b6f06571877bbe54a07",
  "positions": [
    {
      "address": "0x322f0929c4625ed5bad873c95208d54e1c003b2d",
      "symbol": "TSLA",
      "name": "Tesla",
      "rawBalance": "2798573630490006076",
      "balance": "2.798573630490006076",
      "uiBalance": "2.798573630490006076",
      "priceUsd": 386.21,
      "valueUsd": 1080.83
    }
  ],
  "totalValueUsd": 1080.83
}

Example: GET /v1/balances/{address}/history?ticker=NVDA&limit=1

json
[
  {
    "txHash": "0x7442…",
    "blockNumber": "14552898",
    "logIndex": 27,
    "token": "0xd060…",
    "symbol": "NVDA",
    "from": "0x0000000000000000000000000000000000000000",
    "to": "0x2f4579ca81717d3d61bf8b6f06571877bbe54a07",
    "direction": "in",
    "value": "285061926686193138",
    "valueFormatted": "0.285061926686193138",
    "blockTime": null
  }
]

direction is in or out relative to the queried address, and a from of the zero address is a mint. blockTime is null when the block timestamp has not been backfilled yet.

Example: GET /v1/keys/me

json
{
  "keyPrefix": "scl_ab12cd",
  "tier": "free",
  "walletAddress": null,
  "limits": { "perMinute": 60, "perDay": 20000 },
  "usageToday": 1
}

Errors

StatusCodeMeaning
400invalid_addressThe address in the path is not a valid 0x address.
401invalid_keyThe supplied key is unknown, revoked, or mistyped.
404token_not_foundNo token matches that ticker or address.
429rate_limitedTier limit exceeded. Wait the number of seconds in Retry-After.
json
# 400, malformed address in the path
{"error": "invalid_address"}

# 401, missing, revoked, or mistyped key
{"error": "invalid_key"}

# 404, no token matches that ticker or address
{"error": "token_not_found"}

# 429, tier limit exceeded, comes with a Retry-After header
{"error": "rate_limited", "tier": "free", "retryAfterSeconds": 21}

GraphQL

Planned, not yet published. There is no GraphQL endpoint today; the REST API above is the only query interface. GraphQL is on the roadmap for reads that want a whole portfolio in one round trip. The sketch below shows the intended shape and will change before launch, so build against REST for now.

graphqlIllustrative
# Preview of the intended interface. Not runnable today.
query {
  token(id: "TSLA") {
    symbol
    uiMultiplier
    holders
  }
}

SDK reference

Planned, not yet published. The @scallarhq/sdk package is not on npm yet. It is planned as a thin wrapper whose methods map one to one onto the REST endpoints above, returning exact amounts as strings. Until it ships, call the REST API directly; it needs nothing beyond fetch.

jsIllustrative
// Preview of the intended interface. The package is not on npm yet.
import { createClient } from '@scallarhq/sdk';

const scallar = createClient({ apiKey: process.env.SCALLAR_API_KEY });

const tsla = await scallar.tokens.get('TSLA');        // GET /v1/tokens/TSLA
const events = await scallar.tokens.multipliers('SGOV'); // GET /v1/tokens/SGOV/multipliers

Self hosting

Scallar is open source and runs anywhere Docker runs. A self hosted instance has no rate limits and answers only to you.

1. Clone the repository

bash
git clone https://github.com/scallarhq/scallar.git
cd scallar

2. Write your .env

bash
# .env
DATABASE_URL=postgres://scallar:scallar@db:5432/scallar
RPC_URL=<your Robinhood Chain JSON RPC endpoint>
BLOCKSCOUT_API=<Blockscout API base URL for the chain>
ADMIN_TOKEN=<long random string>
TOKEN_CONTRACT=0xd897a87836e14e618491a8ce19bef4c8bad761de
STAKING_CONTRACT=
VariablePurpose
DATABASE_URLPostgres connection string. The compose file provides the db service.
RPC_URLRobinhood Chain JSON RPC endpoint the worker reads from.
BLOCKSCOUT_APIBlockscout API base URL for the chain, used alongside the RPC.
ADMIN_TOKENSecret that protects the administrative routes of your API.
TOKEN_CONTRACTThe SCALLAR token contract address.
STAKING_CONTRACTStaking contract address. Optional; until it is set, POST /v1/keys/refresh cannot upgrade tiers.

3. Start the stack

The compose file brings up three services: db for Postgres, worker for the indexer, and api for the REST API.

yaml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: scallar
      POSTGRES_PASSWORD: scallar
      POSTGRES_DB: scallar
    volumes:
      - scallar-data:/var/lib/postgresql/data

  worker:
    build: .
    command: npm run worker
    env_file: .env
    depends_on:
      - db
    restart: unless-stopped

  api:
    build: .
    command: npm run api
    env_file: .env
    depends_on:
      - db
    ports:
      - "8080:8080"

volumes:
  scallar-data:
bash
docker compose up -d --build
docker compose logs -f worker

4. Or run it directly

Outside Docker the same three roles are plain npm scripts. Run the migration once, then keep the worker and the API running.

bash
npm install
npm run migrate   # create the database schema
npm run worker    # start the indexer
npm run api       # start the REST API

The worker discovers tokens, follows new blocks, and backfills transfer history per token. Backfilling takes a while over a public RPC endpoint, which is usually the bottleneck; GET /v1/status on your own instance shows how far it has gotten.

Rate limits and versioning

Rate limits

TierPer minutePer dayRequirement
Free6020,000None. Anonymous requests share this tier, keyed by IP.
Builder300200,000Stake 10,000 SCALLAR
Pro1,2002,000,000Stake 100,000 SCALLAR

Staking pays no yield. It exists only to unlock API access, and unstaking drops the key back to the Free tier. After staking, call POST /v1/keys/refresh to recompute your tier from the stake held by the key’s wallet address. Webhooks and exports appear as reserved flags on the tiers and are not implemented yet.

Going over the limit returns a 429 with a Retry-After header:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 21
Content-Type: application/json

{"error": "rate_limited", "tier": "free", "retryAfterSeconds": 21}

Versioning

The API version is pinned in the path prefix, currently /v1. Within a major version changes are additive: new fields and new endpoints may appear, and existing fields keep their names and types. Treat unknown fields as forward compatible and ignore them. A breaking change would ship as a new prefix such as /v2 with the previous version kept alive during a deprecation window.