Skip to main content
Tokenized equities are here. Robinhood Chain - an Arbitrum-based L2 - lists thousands of tokenized stocks and ETFs that trade 24/7 and self-custody in a wallet, and the broader RWA market runs largely on the ERC-3643 permissioned-token standard. A tokenized stock is not just an ERC-20 with a ticker. It is a security, and it needs the data infrastructure a security has always needed: a holder registry (who owns what, right now), a price feed that never closes, and a corporate-actions engine for dividends and splits. This guide builds all three on Goldsky.

The three data services

ServiceProductOutput
Holder registry / cap tableSubgraphs or TurboCurrent balance per holder, per token
24/7 price & trade feedTurboOHLC, VWAP, last trade
Dividends & corporate actionsComposePro-rata stablecoin payouts, splits

Step 1: Maintain a live holder registry

The cap table is the foundation - every downstream service (dividends, reporting, compliance) reads from it. Stream transfers of the tokenized stock and keep a running balance per holder.
holder-registry.yaml
name: tokenized-stock-holders
resource_size: s

sources:
  rh_transfers:
    type: dataset
    dataset_name: robinhood_testnet.erc20_transfers
    version: 1.0.0
    start_at: earliest

transforms:
  # Every transfer moves a balance: CREDIT the recipient (+amount) and
  # DEBIT the sender (-amount). Emitting both rows is what makes the
  # running balance net correctly - a credit-only stream overstates holdings.
  balance_deltas:
    type: sql
    primary_key: id
    sql: |
      -- Credit: tokens received (also covers mints, where the sender is the zero address)
      SELECT
        CONCAT(id, '-in')            AS id,
        lower(recipient)             AS holder,
        lower(address)               AS token,
        CAST(amount AS DECIMAL(38, 0)) AS delta,
        _gs_op
      FROM rh_transfers
      WHERE recipient <> '0x0000000000000000000000000000000000000000'

      UNION ALL

      -- Debit: tokens sent (also covers burns, where the recipient is the zero address)
      SELECT
        CONCAT(id, '-out')           AS id,
        lower(sender)                AS holder,
        lower(address)               AS token,
        -CAST(amount AS DECIMAL(38, 0)) AS delta,
        _gs_op
      FROM rh_transfers
      WHERE sender <> '0x0000000000000000000000000000000000000000'

sinks:
  # The aggregate sink sums delta per (holder, token) via a DB trigger,
  # maintaining the current balance incrementally.
  holdings:
    type: postgres_aggregate
    from: balance_deltas
    schema: registry
    landing_table: holdings_log
    agg_table: holdings
    primary_key: id
    secret_name: MY_POSTGRES
    group_by:
      holder:
        type: text
      token:
        type: text
    aggregate:
      balance:
        from: delta
        fn: sum
        type: numeric(38,0)
Two details make this correct: each emitted row gets a unique id (-in/-out suffix) so the landing table never dedupes distinct transfers, and the zero address is excluded so mints and burns adjust only the real holder. Filter out balance = 0 rows when you read the holdings table to get the live holder set.
For ERC-3643 securities, decode the raw_logs instead and also capture identity events (IdentityRegistered) so your registry links each holder to a verified identity, not just an address. A Subgraph is often the cleaner home for a registry you query by holder - it gives you a GraphQL API for statements and positions with no extra service. Confirm the dataset slug for your chain in the Datasource explorer.
The registry answers the questions a transfer agent lives on: who holds this security, how much, and since when - as of the latest block, not last night’s snapshot.

Step 2: Publish a 24/7 price feed

Tokenized equities trade around the clock, so the “closing price” is a live figure. Index trades from the venue (a DEX pool or the chain’s matching contract) and roll them into OHLC/VWAP. This is the DEX trades pattern applied to a stock token; write to ClickHouse for fast time-series queries, or maintain a live last-price with the PostgreSQL aggregate sink.
-- 1-minute OHLC from a trades table
SELECT
  token,
  toStartOfMinute(traded_at) AS minute,
  argMin(price, traded_at)    AS open,
  max(price)                  AS high,
  min(price)                  AS low,
  argMax(price, traded_at)    AS close,
  sum(quantity)               AS volume
FROM trades
GROUP BY token, minute;

Step 3: Distribute dividends and corporate actions

This is where the cap table pays off - literally. A Compose task reads the holder registry at a record-date block and pays each holder their pro-rata dividend in a standard ERC-20 stablecoin, recording the batch in your ledger for reconciliation.
src/tasks/pay-dividend.ts
import type { TaskContext, Chain } from "compose";

// Dividends must go out on the SAME network the stock token lives on - the
// chain this guide indexes holders from. Robinhood Chain (an Arbitrum Orbit L2)
// isn't a built-in Edge RPC chain yet, so define it as a custom chain and fill
// in its id/RPC. Swap for evm.chains.<name> once it's available on Edge.
const STOCK_CHAIN: Chain = {
  id: 0, // Robinhood Chain testnet chain ID
  name: "Robinhood Chain Testnet",
  testnet: true,
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: {
    default: { http: ["https://<robinhood-chain-rpc>"] },
    public: { http: ["https://<robinhood-chain-rpc>"] },
  },
  blockExplorers: { default: { name: "Explorer", url: "https://<explorer>" } },
};

export async function main(
  context: TaskContext,
  payload: { token: string; dividendPerToken: string; recordBlock: number },
) {
  const { fetch, evm } = context;

  // Holders as of the record-date block, from your registry service
  const holders = await fetch<{ address: string; balance: string }[]>(
    `https://registry.example.com/holders?token=${payload.token}&block=${payload.recordBlock}`,
  );

  const wallet = await evm.wallet({ name: "dividend-payer", sponsorGas: true });

  const results = await Promise.allSettled(
    holders.map((h) => {
      const amount = (BigInt(h.balance) * BigInt(payload.dividendPerToken)) / 10n ** 18n;
      // Robinhood Chain stablecoins are ERC-20 - pay with the standard transfer.
      return wallet.writeContract(
        STOCK_CHAIN,
        "0xDIVIDEND_STABLECOIN",
        "transfer(address,uint256)",
        [h.address, amount],
      );
    }),
  );

  const paid = results.filter((r) => r.status === "fulfilled").length;
  return { holders: holders.length, paid };
}
Pay on the network your tokenized stock lives on. This task indexes holders from Robinhood Chain, so STOCK_CHAIN must be Robinhood Chain - sending to any other network (the example previously defaulted to Arbitrum) would deliver dividends to look-alike addresses that never held the stock.
Every payout is durable (it completes through failures) and traced end to end. For stock splits, reverse splits, and mergers, follow the corporate-actions distributor guide, which handles the ratio math and multi-step distributions.

Step 4: Reconcile with your transfer agent

Because the registry lives in your database, reconciling the on-chain holder list against your books of record is a query, not a project - the same real-time reconciliation pattern, applied to positions instead of payments. Breaks (an address holding tokens with no matching book entry, or vice versa) surface immediately.

Business outcomes

  • A real-time cap table replaces end-of-day position files.
  • A market that never closes gets a price feed that never closes.
  • Automated, auditable corporate actions - dividends and splits execute as code, with a trace for every payment.
  • Chain-agnostic - the same pattern covers Robinhood Chain, ERC-3643 securities on Ethereum/Polygon, and RWA chains like Plume and Mantra.

Resources

Can’t find what you’re looking for? Reach out to us at support@goldsky.com for help.