robinhood-chain-kit
v0.9.0
Published
ERC-8056 scaled-UI stock tokens, peer-to-peer settlement, transaction preflight, Oracle Guard, chain constants, and resilient RPC helpers for Robinhood Chain (chain id 4663).
Downloads
302
Maintainers
Keywords
Readme
// npm i robinhood-chain-kit viem
import { createPublicClient, http } from 'viem'
import { robinhoodChain } from 'robinhood-chain-kit'
import { robinhoodChainActions } from 'robinhood-chain-kit/viem'
const client = createPublicClient({ chain: robinhoodChain, transport: http() }).extend(robinhoodChainActions())
console.log(await client.getEquityQuote('AAPL'))That one call resolves the ticker through Robinhood's own token registry,
matches the Chainlink feed, reads the round, the authoritative ERC-8056
multiplier, and the corporate-action pause flag from the contracts, and returns
token price, underlying share price, session context, and a fail-closed health
verdict. viem is an optional peer used by the /viem entry alone — and
only for types, so the core package still has zero runtime dependencies
and works with any client that can execute a read call.
What touches the network
Everything in this package is pure computation except the functions below. Nothing here ever signs, sends, or holds a key.
| Function | Talks to | Via |
|---|---|---|
| getEquityQuote / getOracleHealth / getScaledUiMultiplierState / getStockDirectory (viem actions) | chain 4663 + both directories | your client + fetch |
| readScaledUiMultiplierState(s), readOraclePauseState | stock-token contracts | your client |
| readOracleRound(s), readSequencerUptime, checkOracleHealth | Chainlink feeds (+ token) | your client |
| executeContractCalls | whatever calls you give it | your client |
| loadStockTokenDirectory | api.robinhood.com/rhj/assets | fetch (injectable) |
| loadChainlinkFeedDirectory | the Chainlink reference directory | fetch (injectable) |
| inspectTransaction | read-only evidence | the adapter you supply |
| getLogsPaged | your RPC | the getLogs you supply |
| fetchWithRetry | the URL you pass | fetch (injectable) |
Everything else — decoding, multiplier math, settlement pairing, premium and
deviation arithmetic, session calendars, health assessment — is [pure] and
runs the same offline. llms.txt in the package root lists every export with
its [pure]/[network] marker, one line each, for token-budgeted contexts.
Oracle health in three lines
assessOracleHealth has always failed closed on unknown sequencer and pause
state — and 0.8.x gave you no way to obtain either input. Now the doors have
on-ramps:
const [feed] = findChainlinkFeeds(await loadChainlinkFeedDirectory(), { baseAsset: "AAPL", quoteAsset: "USD" });
const token = findStockToken(await loadStockTokenDirectory(), "AAPL");
const { health } = await checkOracleHealth(client, { feed: feed!, token: token!.tokenAddress });One call reads the round, the token's on-chain oraclePaused() flag
(documented on the official oracles page and verified against live contracts),
and — when you pass one — an L2 sequencer uptime feed. The verdict is the same
fail-closed assessment: on a healthy feed with no sequencer source you get
usable: false, issues: ["sequencer-state-unknown"], which is the honest
statement of what you have not verified. No uptime feed address for chain 4663
is listed in the live Chainlink directory, so the kit ships none — supply
sequencerFeed when Chainlink publishes one, or sequencer from your own
liveness monitoring.
The multiplier in one
const { schedule } = await readScaledUiMultiplierState(client, stockToken);
// schedule.multiplier is the contract's own uiMultiplier();
// schedule.status says whether a figure computed from it is safe right nowreadScaledUiMultiplierStates(client, tokens) batches the whole directory
through Multicall3 (falling back to sequential reads on clients without it),
isolating per-token failures. The client in all of these is structural — any
viem PublicClient satisfies it, and so does anything else with a
readContract — because the kit still imports no client library.
Why
Every project on this chain re-derives the same primitives: network and bridge configuration, trusted DEX event scopes, US regular-session context, independent oracle-heartbeat checks, and resilient RPC/HTTP access. This package keeps those primitives typed, tested, and free of runtime dependencies.
It also carries the one thing that makes this chain different from every other L2, and the one that costs the most to learn the hard way: tokenized equities adjust through an ERC-8056 scaled-UI multiplier rather than by minting, and the deployed contracts do not use the event name the EIP specifies.
Three distinctions run through that module, because collapsing any of them produces a financial figure that is wrong and looks right: a raw ERC-20 amount is not a displayed share count; a multiplier estimated from a transfer is not the authoritative one on the contract; and the ERC-8056 draft is not the deployed behaviour. Where the specification and a deployment disagree, this package follows the deployment and says which is which.
What's inside
| Module | What it gives you |
|---|---|
| chain | mainnet + testnet RPC, sequencer feed, explorer, separate Blockscout /api + REST /api/v2, and viem-compatible chain objects |
| bridge | documented mainnet/testnet L1 + L2 protocol contracts, Arbitrum precompiles, and gateway-emitted deposit/withdrawal events |
| dex | V2/V3/V4 event signatures, explicit caller/recipient roles, and address-scoped factory/PoolManager discovery targets |
| stocktokens / ERC-8056 | the transfer event stock tokens actually emit and its verified topic0, client-neutral ABI with per-member provenance, bounded multiplier estimation with exact feasible intervals, raw↔display conversion, corporate-action detection that will not fire on estimate noise, and peer-to-peer settlement reconstruction priced per raw or per displayed unit |
| feeds / Oracle Guard | strict Chainlink directory loading, typed feed lookup and read calls, exact bigint price formatting, and fail-closed round/sequencer/pause assessment |
| registry | Robinhood's own stock-token address book (api.robinhood.com/rhj/assets), parsed atomically with optional byte pinning, plus the verified USDG cash-asset constants |
| executed readers | readScaledUiMultiplierState(s), readOraclePauseState, readOracleRound(s), readSequencerUptime, checkOracleHealth — the on-ramps that turn exported call descriptors into typed live state, over any structural client |
| premium | underlyingSharePrice (the feed prices the token, not the share — the docs' 1e18 formula, encoded), signed premiumBps, exact deviation math without the preflight engine |
| preflight / Transaction Firewall | strict action decoding, injected simulation and identity evidence, exact balance/fee/market checks, and withheld-or-ready transaction plans |
| robinhood-chain-kit/viem | robinhoodChainActions() for .extend() and createViemPreflightAdapter — the shipped viem bindings; viem stays an optional, types-only peer |
| getLogsPaged(getLogs, from, to, opts) | exact-block-count log backfill: shrinks only on recognized size errors, bounds 429 retries, supports cancellation, and never retries consumer callbacks |
| getUsEquityMarketSession(ts) | DST-, holiday-, and early-close-aware US regular-session context |
| isFeedFresh(updatedAt, heartbeat, now) | heartbeat freshness independent from the regular session; Stock Token feeds currently publish 24/5 |
| fetchWithRetry(url, init, opts) | caller-aware timeout, Retry-After + jittered backoff, body cleanup, and safe-method retry defaults |
How it fits together
source: assets/architecture.mmd — rendered to SVG so it displays on npm too (npm does not render mermaid)
Install
npm i robinhood-chain-kitNode 20.3+ is supported. Both ESM imports and CommonJS require() are
published, from the root and from four subpaths — ./viem, ./erc8056,
./oracle, ./preflight — so a bundle that only guards prices does not carry
the preflight engine. The root re-exports everything except the viem entry.
viem is an optional peer: install it for the /viem bindings, skip it and
everything else works unchanged.
Usage
import {
robinhoodChain,
L1_GATEWAYS,
DEPOSIT_INITIATED_EVENT,
getUsEquityMarketSession,
isFeedFresh,
} from "robinhood-chain-kit";
import { createPublicClient, http, parseAbiItem } from "viem";
import { mainnet } from "viem/chains";
const l2 = createPublicClient({ chain: robinhoodChain, transport: http() });
// track recent ERC-20 deposits from an L1 RPC
const l1 = createPublicClient({ chain: mainnet, transport: http() });
const l1Head = await l1.getBlockNumber();
const deposits = await l1.getLogs({
address: [...L1_GATEWAYS],
event: parseAbiItem(DEPOSIT_INITIATED_EVENT),
fromBlock: l1Head - 1_000n,
toBlock: l1Head,
});
// regular-session context and feed freshness are separate signals
function shouldCompareDuringRegularSession(updatedAt: number, heartbeat: number) {
const now = Date.now() / 1000;
const session = getUsEquityMarketSession(now);
const freshness = isFeedFresh(updatedAt, heartbeat, now);
return session.phase === "regular" && freshness.fresh;
}getUsEquityMarketSession models scheduled US exchange holidays and standard
13:00 ET early closes. It cannot predict emergency closures. The current
Robinhood Stock Token feeds are documented as 24/5, so do not use the regular
session as a feed-freshness proxy: check updatedAt, the feed heartbeat,
sequencer uptime, and the token's corporate-action pause state.
nextUsEquitySessionChange(ts) returns the exact epoch of the next scheduled
boundary and which way it flips — { at, to: "open" | "closed" } — holiday-
and early-close-aware, so the Friday after Thanksgiving counts down to 13:00
ET and a Friday evening before a Monday holiday counts down to Tuesday's open.
Backfill event history without babysitting the RPC
The public RPC caps eth_getLogs at ~10k blocks and ~10k results, and rate
limits on top. getLogsPaged encodes the response that actually works (learned
running rhxbt.com's full-chain swap index): a "range too large" error is a
size problem — halve the window; a 429 is a time problem — cool off and
retry the same window, because halving multiplies the request count and makes
it worse. After a reduction it re-grows only after 3 clean passes, so it never
ping-pongs against the cap. Authentication, malformed-query, and other fatal
errors are rethrown immediately instead of being disguised as size failures.
import { getLogsPaged, robinhoodChain, V2_PAIR_CREATED_EVENT } from "robinhood-chain-kit";
import { createPublicClient, http, parseAbiItem } from "viem";
const client = createPublicClient({ chain: robinhoodChain, transport: http() });
const head = await client.getBlockNumber();
const abortController = new AbortController();
// every V2 pair ever created, from genesis, over the plain public RPC
const pairs = await getLogsPaged(
(fromBlock, toBlock) =>
client.getLogs({ event: parseAbiItem(V2_PAIR_CREATED_EVENT), fromBlock, toBlock }),
0n,
head,
{ chunkSize: 9_500n, maxRetries: 5, signal: abortController.signal },
);Works with any client — you supply the getLogs function, the kit supplies the
judgment. For huge ranges, stream with { collect: false, onPage } instead of
accumulating. chunkSize is an exact block count: 10_000n requests at most
10,000 inclusive blocks.
Discover DEX pools without trusting event signatures
An event topic is not a deployment identity: any contract can emit a matching topic. Supply independently verified factory/PoolManager addresses and use the returned address-scoped targets:
import { getDexDiscoveryTargets } from "robinhood-chain-kit";
const targets = getDexDiscoveryTargets({
v2Factories: ["0x..."],
v3Factories: ["0x..."],
v4PoolManagers: ["0x..."],
});V2 to and V3 recipient are output recipients, not guaranteed economic
actors. V4 sender is normally periphery. Treat transaction origin, routers,
smart accounts, and beneficiaries as separate roles.
Read a tokenized equity without getting the split wrong
Corporate actions on this chain do not mint or burn. The contract carries a multiplier, raw balances stay exactly where they were, and the amount a holder is shown changes underneath them. Every transfer emits both numbers.
The event is not the one EIP-8056 names. The spec says
TransferWithUIAmount; the deployed contracts emit TransferWithScaledUI. A
filter built from the spec computes a different topic0 and matches zero logs,
forever. That failure is silent and it does not look like a bug — it looks like
a chain with no tokenized-equity activity on it.
A multiplier recovered from a transfer is an estimate, not a reading. The
contract computes uiValue = floor(rawValue × multiplier / 1e18), so the
low-order information is destroyed before the event exists. A true multiplier
of 1.5 with a raw value of 3 emits floor(4.5) = 4; dividing back gives
1.333…, which is not the multiplier. What can be recovered exactly is the
interval.
import { parseAbiItem } from "viem";
import {
TRANSFER_WITH_SCALED_UI_EVENT,
estimateScaledUiMultiplier,
formatScaledUiMultiplier,
} from "robinhood-chain-kit";
const logs = await client.getLogs({
address: stockToken,
event: parseAbiItem(TRANSFER_WITH_SCALED_UI_EVENT),
fromBlock,
toBlock,
});
const { value, uiValue } = logs.at(-1)!.args;
const estimate = estimateScaledUiMultiplier({ value, uiValue });
if (!estimate.estimated) throw new Error(`cannot bound the multiplier: ${estimate.reason}`);
formatScaledUiMultiplier(estimate.multiplier); // display value only
estimate.lowerBound; // hard fact
estimate.upperBoundExclusive; // hard factThe interval narrows as the raw amount grows and collapses to a single value
once the transfer is large relative to 1e18. A whole-token transfer of an
18-decimal token pins the multiplier exactly; a three-unit transfer bounds it
between 1.33 and 1.67.
For a value you can put in a financial calculation, read the contract.
import {
ERC8056_VERIFICATION,
getErc8056ReadCalls,
resolveScaledUiMultiplier,
} from "robinhood-chain-kit";
const [current, pending, effectiveAt] = await Promise.all(
getErc8056ReadCalls(stockToken).map((c) =>
client.readContract({ address: c.address, abi: c.abi, functionName: c.functionName }),
),
);
resolveScaledUiMultiplier({ current, pending, effectiveAtSeconds: effectiveAt }, nowSeconds);
// -> { status: "none" | "applied", multiplier }
// -> { status: "pending", multiplier, pending, effectiveAtSeconds } not live yet
// -> { status: "due", multiplier, pending, effectiveAtSeconds } ambiguousnewUIMultiplier() is a scheduled value, not the current one.
UIMultiplierUpdated announces a change; one token on chain emitted the
identical (old, new, effectiveAt) triple at two different blocks while old
was still live. Reading the pending value as current prices a split before it
happens. status: "due" is the state worth handling — the effective time has
passed and the contract still reports the old value, so anything derived from
either is ambiguous.
Every ABI member is verified against live contracts on chain 4663, and
ERC8056_VERIFICATION says how: deployed-call for the three reads,
deployed-logs for both events. UIMULTIPLIER_UPDATED_TOPIC0 ships only
because it was earned — 0.7.0 withheld it, because a topic0 nobody has
confirmed fails silently and that is the trap this module exists for.
Detect a corporate action without inventing one
Two transfers of different sizes under one unchanged multiplier produce different point estimates, because each was truncated differently. Comparing those points reports a corporate action every time the transfer sizes differ.
import { compareScaledUiEstimates, compareScaledUiMultiplier } from "robinhood-chain-kit";
// estimates: compares BOUNDS, and only claims a change when no single
// multiplier could have produced both observations
compareScaledUiEstimates(before, after);
// -> { comparable: true, moved: false } ranges overlap: unproven
// -> { comparable: true, moved: true, direction } ranges disjoint: proven
// authoritative contract readings only
compareScaledUiMultiplier(previousMultiplier, currentMultiplier);moved: false from compareScaledUiEstimates means not provable from these
two observations, not definitely unchanged. A small adjustment inside two
wide ranges is invisible to it, and the honest way to see that is a contract
read.
Both report direction only. A split and a dividend adjustment both move the multiplier up; nothing in the observation separates them.
See the trades no DEX index can
A stock-token transfer and a stablecoin transfer inside the same transaction, opposed between the same two addresses, is a trade by construction: one side gave up the asset, the other gave up the cash, and both legs committed together or neither did. No pool was involved, which is exactly why a swap-based index cannot see any of it.
import {
inferRawSettlementPrice,
inferUiSettlementPrice,
pairDeliveryVersusPayment,
settlementLegFromScaledUiLog,
settlementLegFromTransferLog,
} from "robinhood-chain-kit";
// decoded logs -> legs, with the two hand-written mistakes designed out:
// the raw value lands where SettlementLeg requires it, and the contract's
// exact uiValue rides along for UI pricing instead of being dropped
const stockLegs = stockLogs.map(settlementLegFromScaledUiLog);
const cashLegs = usdgLogs.map(settlementLegFromTransferLog); // address-filter these first
const settlements = pairDeliveryVersusPayment({ stockLegs, cashLegs });
const scales = { stockDecimals: 18, cashDecimals: 6, priceDecimals: 2 };
for (const settlement of settlements) {
// per RAW ERC-20 unit
inferRawSettlementPrice({ settlement, ...scales });
// -> { inferred: true, price: "400.00", decimals: 2, stockAmountMode: "raw" }
// per DISPLAYED share — pass the log's own uiValue, or an authoritative
// multiplier. both, or neither, throws.
inferUiSettlementPrice({ settlement, ...scales, stockUiValue });
// -> { inferred: true, price: "100.00", decimals: 2, stockAmountMode: "ui" }
}settlement.stock.value is a raw ERC-20 amount, not a share count. Under a
4x multiplier one raw unit is four displayed shares, so the same trade is 400
per raw unit and 100 per share. Quoting the wrong one is a 4x error in a price,
which is why the basis is in the result rather than in a comment.
Pairing fails closed, hard. A settlement is reported only when a transaction carries exactly one stock leg and one cash leg, opposed between the same pair of addresses. Batches, routed fills, and anything with a third party in the middle are left unclassified rather than guessed at.
inferred: true is not decoration. No venue quoted this price. It is the
ratio of two amounts in one transaction, and the two parties may have agreed it
hours earlier, off-chain, at a level unrelated to any market. A count of
settlements is a hard fact; the level any one of them printed at is a
reconstruction, and anything displaying it should say so.
Guard a Chainlink price round
Oracle Guard loads a stable, validated subset of the live Chainlink directory
and keeps answers as bigint until exact decimal formatting. A round is usable
only when its answer and timestamp are valid, its heartbeat is fresh, the
sequencer is up beyond the caller's recovery grace period, and corporate-action
state is explicitly active or not applicable.
import {
assessOracleHealth,
findChainlinkFeeds,
loadChainlinkFeedDirectory,
} from "robinhood-chain-kit";
const feeds = await loadChainlinkFeedDirectory();
const [feed] = findChainlinkFeeds(feeds, { baseAsset: "GOOGL", quoteAsset: "USD" });
if (!feed) throw new Error("GOOGL/USD feed is missing");
const health = assessOracleHealth({
feed,
round: {
roundId: 10n,
answer: 12_345_678_900n,
updatedAtSeconds: 1_000n,
answeredInRound: 10n,
},
nowSeconds: 1_200,
sequencer: { status: "up", sinceSeconds: 100, gracePeriodSeconds: 60 },
pauseState: "active",
});
if (!health.usable) console.error(health.issues);
else console.log(health.formattedAnswer); // 123.45678900Remote metadata is a trust boundary. parseChainlinkFeedDirectory proves the
document is well-shaped; it cannot prove it is the one the operator published,
and a hijacked mirror can serve a schema-perfect directory pointing every feed
at addresses it chose. Pin the bytes when the contents are load-bearing:
await loadChainlinkFeedDirectory({ expectedSha256: "<64 hex chars>" });The digest is taken over the raw response body before parsing, and no value is hardcoded in this package — a pinned constant nobody maintains breaks for every consumer at once, so it is the caller's decision and the caller's upkeep.
pauseState: "unknown" and sequencer: { status: "unknown" } fail closed.
Use "not-applicable" only for assets that do not have corporate-action
pauses. Oracle Guard reports safety inputs; it never emits buy/sell decisions
or invents fallback prices.
When you want the inputs gathered for you — the round executed, the pause flag
read from the token, the uptime feed consulted — that is
checkOracleHealth, which feeds this same
assessment.
Preflight a transaction before signing
Transaction Firewall separates a transaction's claimed calldata shape from independent evidence about the RPC chain, simulation, target bytecode, contract and asset identity, approval spender, balances, allowances, gas, fees, Oracle Guard state, and market execution risk. Caller-supplied gas and fee caps are never accepted as estimates: they are compared with independent adapter evidence before a plan can become ready.
import { inspectTransaction } from "robinhood-chain-kit";
const report = await inspectTransaction(
{
chainId: 4663,
from: "0x1111111111111111111111111111111111111111",
to: "0x2222222222222222222222222222222222222222",
value: 1_000_000_000_000_000n,
},
{
adapter: {
getChainId: () => publicClient.getChainId(),
simulate: async (request) => {
try {
await publicClient.call({
account: request.from,
to: request.to,
data: request.data,
value: request.value,
});
return { success: true };
} catch (error) {
return { success: false, revertReason: String(error) };
}
},
getCode: async (address) =>
(await publicClient.getBytecode({ address })) ?? "0x",
getNativeBalance: (owner) => publicClient.getBalance({ address: owner }),
estimateGas: (request) =>
publicClient.estimateGas({
account: request.from,
to: request.to,
data: request.data,
value: request.value,
}),
getFeePerGas: () => publicClient.getGasPrice(),
},
},
);
if (report.verdict !== "safe") console.error(report.issues);
else console.log(report.plan.steps); // prepared only; never signed or sentWith viem, the adapter above is one call —
createViemPreflightAdapter(publicClient) from robinhood-chain-kit/viem —
which implements every method viem makes trivial and deliberately omits
resolveContract/resolveAsset rather than fabricating verified: true
evidence from an RPC that cannot know it.
The default policy allows only Robinhood mainnet/testnet, requires RPC chain
identity, simulation, gas, fee and balance evidence, blocks unlimited token and
operator approvals, blocks EOA or unknown/unverified approval targets, and
treats unknown actions as unknown. The approval-target checks can be relaxed
only with the explicit allowEoaApprovalTargets or
allowUnverifiedApprovalTargets policy flags.
Custom decoders can describe verified DEX or bridge calls and their
balance/allowance requirements. Their selector must match the calldata, their
requirements are bounded and deduplicated, and swaps must declare both an input
asset and a canonical marketPair whose base matches that input (native assets
use native:<chainId>). Every Oracle, DEX, slippage, and
price-impact observation for a market-sensitive action carries source,
chainId, blockNumber, observedAtSeconds, baseAsset, and quoteAsset.
Pair/chain mismatch, stale or future observations, and excessive timestamp or
block skew fail closed. Selector decoding never proves contract identity.
Reports are immutable evidence snapshots. A safe report contains one
prepared transaction step; blocked and unknown reports always withhold all
steps. The module has no signer, wallet, private-key, broadcast, or send API.
Examples
Runnable scripts in examples/:
watch-bridge.mts— recent canonical ERC-20 deposits, with each token's actual decimalsscan-pools.mts— V2/V3/V4 discovery from verified deployment addresses, with streaming and resume boundsequity-quote.mts— the five-line viem on-ramp: one composed live quote with the honest remaining unknown printedoracle-health-live.mts—checkOracleHealthagainst mainnet, and why no sequencer feed address shipsviem-preflight.mts— the whole preflight adapter in onecreateViemPreflightAdaptercallmarket-session.mts— report regular-session context without claiming oracle freshnessstock-token-transfers.mts— bound a stock token's ERC-8056 multiplier from live logs, prove a corporate action from disjoint ranges, and list the authoritative contract readsoracle-guard.mts— read a live feed and fail closed on unknown sequencer or pause statepreflight-transaction.mts— simulate and risk-check a native transfer without a signer or send callpreflight-erc20-transfer.mts— decode and approve a safe ERC-20 transfer plan using deterministic evidencepreflight-approval-risk.mts— see an unlimited approval to an unverified spender fail closedpreflight-custom-swap.mts— bind a custom swap to balances, allowance, Oracle Guard, and market provenance
The transaction preflight examples guide provides the recommended learning order and expected verdict for each example.
cd examples && npm i
V2_FACTORIES=0x... V3_FACTORIES=0x... V4_POOL_MANAGERS=0x... npx tsx scan-pools.mts
ETHEREUM_RPC_URL=https://your-rpc.example npx tsx watch-bridge.mts
npx tsx oracle-guard.mts # defaults to unknown pause/sequencer state and fails closed
STOCK_TOKEN=0x... npx tsx stock-token-transfers.mts
FROM_ADDRESS=0x... TO_ADDRESS=0x... npx tsx preflight-transaction.mts
npm run preflight:erc20
npm run preflight:approval-risk
npm run preflight:swapNotes
- Unit-tested (
npm test), package-linted, example-type-checked, and CI-tested at the Node 20.3 floor and on Node 22. npm run verifyruns the build, unit, package-resolution, and example checks.- Releases are published by hand and attested by CI.
npm run releaseruns every gate and then publishes; pushing the matchingv*tag starts a workflow that rebuilds the tag's tree from scratch and fails unless the tarball npm is serving is byte-identical to it. That catches what a manual release actually gets wrong — a dirty tree, a staledist/, the wrong branch — which publishing from CI never checked either. Versions carry no npm provenance attestation: trusted publishing was rejected on two attempts and none of the earlier versions had one to lose. - Contract addresses come from the official chain docs — verify independently before moving value.
- This is an independent, unofficial community project. Not affiliated with or endorsed by Robinhood.
Links
Plain markdown on purpose: npm sanitizes raw HTML, so the centered footer this replaced rendered on GitHub and quietly vanished on npmjs.com.
| | | |---|---| | Live terminal | rhxbt.com | | Author | @mkrz_ on X | | The agent this was extracted from | @0xrhXBT on X | | Source | github.com/mkrz-x/robinhood-chain-kit | | Package | npmjs.com/package/robinhood-chain-kit |
Credits
Built by @mkrz-x (rhxbt.com) with review help from @aididhaiqal.
