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.
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
| Reading | Generic EVM indexer | Scallar |
|---|---|---|
| rawBalance | 100.000000000000000000 | 100.000000000000000000 |
| uiMultiplier before block 4,629,631 | not read | 1.0 |
| uiMultiplier after block 4,629,631 | not read | 1.000957519890990718 |
| uiBalance reported | 100.000000000000000000 | 100.095751989099071800 |
| Change attributed | none, 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
uiBalancenext 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
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:
{
"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
curl https://api.scallar.finance/v1/tokens/TSLA \
-H "x-api-key: scl_ab12cd34ef56ab78cd90ef12ab34cd56ef78ab90"3. Read the response
{
"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.
// 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
// 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| Token | rawBalance | uiMultiplier | uiBalance |
|---|---|---|---|
| TSLA | 2.798573630490006076 | 1.0 | 2.798573630490006076 |
| SGOV | 100.000000000000000000 | 1.000957519890990718 | 100.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:
[
{
"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.
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
| Method | Path | Description |
|---|---|---|
| GET | /v1/status | Indexer snapshot: token count, how many are backfilled, and the last indexed block range. |
| GET | /v1/tokens | Every 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}/multipliers | Multiplier history for a token, newest first: old value, new value, block, transaction hash, change percent. |
| GET | /v1/tokens/{id}/holders | Top 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}/history | Transfer history for an address, newest first. Filter by ticker, cap with limit. |
| POST | /v1/keys | Create a free API key. Body accepts optional label and walletAddress. Rate limited by IP. |
| GET | /v1/keys/me | Inspect the calling key: prefix, tier, limits, and usage today. Requires auth. |
| POST | /v1/keys/refresh | Recompute the key tier from staked SCALLAR once the staking contract is configured. Requires auth. |
Query parameters
| Parameter | Type | Applies to | Description |
|---|---|---|---|
| limit | integer | holders, history | Maximum number of rows returned, for example ?limit=50. |
| ticker | string | history | Restrict the event stream to one Stock Token, for example NVDA. |
Example: GET /v1/status
{
"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:
[
{
"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
[
{
"holder": "0x2f4579ca81717d3d61bf8b6f06571877bbe54a07",
"rawBalance": "745903681766298514678",
"balance": "745.903681766298514678",
"uiBalance": "745.903681766298514678",
"share": 27.23777270562604
}
]Example: GET /v1/balances/{address}
{
"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
[
{
"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
{
"keyPrefix": "scl_ab12cd",
"tier": "free",
"walletAddress": null,
"limits": { "perMinute": 60, "perDay": 20000 },
"usageToday": 1
}Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_address | The address in the path is not a valid 0x address. |
| 401 | invalid_key | The supplied key is unknown, revoked, or mistyped. |
| 404 | token_not_found | No token matches that ticker or address. |
| 429 | rate_limited | Tier limit exceeded. Wait the number of seconds in Retry-After. |
# 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.
# 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.
// 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/multipliersSelf 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
git clone https://github.com/scallarhq/scallar.git
cd scallar2. Write your .env
# .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=| Variable | Purpose |
|---|---|
| DATABASE_URL | Postgres connection string. The compose file provides the db service. |
| RPC_URL | Robinhood Chain JSON RPC endpoint the worker reads from. |
| BLOCKSCOUT_API | Blockscout API base URL for the chain, used alongside the RPC. |
| ADMIN_TOKEN | Secret that protects the administrative routes of your API. |
| TOKEN_CONTRACT | The SCALLAR token contract address. |
| STAKING_CONTRACT | Staking 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.
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:docker compose up -d --build
docker compose logs -f worker4. 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.
npm install
npm run migrate # create the database schema
npm run worker # start the indexer
npm run api # start the REST APIThe 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
| Tier | Per minute | Per day | Requirement |
|---|---|---|---|
| Free | 60 | 20,000 | None. Anonymous requests share this tier, keyed by IP. |
| Builder | 300 | 200,000 | Stake 10,000 SCALLAR |
| Pro | 1,200 | 2,000,000 | Stake 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/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.