KIV protocol
KIV records a market call before its outcome exists, then settles it against a Chainlink price feed on Robinhood Chain. It holds no funds, requests no approvals, and has no owner function that can change a result.
A call is four values: an asset's price feed, a target level, a direction, and a deadline. Writing it costs one transaction. After the deadline, anyone can settle it, and the contract compares the feed's reported price to the target and writes Hit or Miss permanently.
Everything a client needs is emitted as events. There is no off-chain database of record, no API key, and no paid data dependency.
Permission surface. The only thing a user ever signs is a call. The contract has no transferFrom, no approve flow, and no payable function.
Network
KIV is deployed on Robinhood Chain, an Arbitrum Nitro Layer 2 that settles to Ethereum and uses ETH for gas. It is fully EVM compatible, so Foundry, Hardhat, viem, ethers and wagmi work unmodified.
| Property | Mainnet | Testnet |
|---|---|---|
| Chain ID | 4663 | 46630 |
| Gas token | ETH | ETH |
| Public RPC | https://rpc.mainnet.chain.robinhood.com | https://rpc.testnet.chain.robinhood.com |
| Explorer | robinhoodchain.blockscout.com | explorer.testnet.chain.robinhood.com |
| KIV contract | Published at launch | Published at launch |
Public RPC is rate limited. Fine for development and for a client reading one record at a time. For indexing or anything production-facing, use a provider endpoint — Alchemy, Chainstack, QuickNode and dRPC all support the chain.
Quickstart
Add the chain, point a client at it, and read a record. No account, no key.
import { createPublicClient, http, defineChain } from 'viem' export const robinhoodChain = defineChain({ id: 4663, name: 'Robinhood Chain', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: ['https://rpc.mainnet.chain.robinhood.com'] } }, blockExplorers: { default: { name: 'Blockscout', url: 'https://robinhoodchain.blockscout.com' }, }, }) export const client = createPublicClient({ chain: robinhoodChain, transport: http(), })
Data model
One struct, stored in an append-only array. The array index is the call ID, which is also what a public record URL points at.
enum Status { Open, Hit, Miss } struct Call { address author; // who signed it address feed; // Chainlink aggregator for the asset int256 target; // price level, in the feed's decimals bool above; // true = "closes above", false = "below" uint64 deadline; // unix seconds uint64 sealedAt; // block.timestamp at creation — the proof int256 settledAt; // price written at settlement, 0 while Open Status status; }
Why sealedAt is the whole product
A balance snapshot proves nothing, because balances can be borrowed for a single block. A call is different: it has to be published before the market answers. sealedAt is written by the chain, not by the author, so a record cannot be backdated by anyone — including whoever operates KIV.
Contract reference
makeCall
function makeCall( address feed, int256 target, bool above, uint64 deadline ) external returns (uint256 id);
Appends a call and emits CallMade. Reverts if the deadline is not in the future, if the feed is not on the allow-list, or if the deadline is further out than MAX_HORIZON.
settle
function settle(uint256 id) external;
Callable by anyone once the deadline has passed. Reads the feed, writes the status and the settled price, and emits Settled. Reverts if the call is already settled, if the deadline has not passed, or if the feed has not updated since the deadline.
settleMany
function settleMany(uint256[] calldata ids) external;
Settles a batch, skipping any ID that is not yet settleable instead of reverting the whole transaction. This is what the scheduler calls.
Views
| Function | Returns |
|---|---|
getCall(uint256 id) | The full Call struct. |
callCount() | Total calls ever made. IDs run 0 … callCount-1. |
record(address who) | (uint32 hits, uint32 misses, uint32 open) for one author. |
isSettleable(uint256 id) | bool — deadline passed and the feed is fresh enough. |
Events
event CallMade( uint256 indexed id, address indexed author, address indexed feed, int256 target, bool above, uint64 deadline, uint64 sealedAt ); event Settled( uint256 indexed id, address indexed author, int256 price, bool hit );
These two events are a complete replica of protocol state. Any client can rebuild every record and every hit rate from logs alone, with no dependency on KIV's servers.
Settlement
Settlement is deliberately dumb. It reads one number and compares it to another.
function settle(uint256 id) public { Call storage c = calls[id]; if (c.status != Status.Open) revert AlreadySettled(); if (block.timestamp < c.deadline) revert TooEarly(); (, int256 price,, uint256 updatedAt,) = AggregatorV3Interface(c.feed).latestRoundData(); // the feed must have moved on since the deadline, // otherwise a frozen oracle could decide the result if (updatedAt < c.deadline) revert StaleFeed(); if (price <= 0) revert BadPrice(); bool won = c.above ? price > c.target : price < c.target; c.settledAt = price; c.status = won ? Status.Hit : Status.Miss; unchecked { won ? tally[c.author].hits++ : tally[c.author].misses++; } emit Settled(id, c.author, price, won); }
Who settles
KIV runs a scheduler that sweeps expired calls and batches them through settleMany. It is a convenience, not a dependency — the function is unrestricted, so a user, a rival client, or a bot can settle any call at any time after its deadline. If KIV shuts down tomorrow, every open record can still be resolved by anyone holding a few cents of ETH.
Ties
A call resolves on a strict inequality. If the settled price lands exactly on the target, an "above" call is a miss. This is stated plainly in the UI when a call is written, because a silent tie rule is the kind of thing people argue about later.
Price feeds
Robinhood Chain publishes a Chainlink feed for each Stock Token as well as feeds for major crypto pairs. Read the price on-chain; do not settle against an HTTP endpoint.
The multiplier trap. Stock Tokens handle dividends and splits through an on-chain multiplier exposed as uiMultiplier() under ERC-8056. Robinhood's /rhj/prices endpoint returns the underlying equity price, which is not the same number as the token price once a corporate action has occurred. The token's Chainlink feed already incorporates the multiplier. Settle against the feed and a split can never flip a hit into a miss.
Discovering assets
The public asset list is free and needs no key. Each entry carries a tradingCapabilities field describing whether the asset trades during market hours, extended hours and overnight — useful for validating that a deadline falls in a session where the asset actually prices.
curl https://api.robinhood.com/rhj/assets
Feed decimals
Targets are stored in the feed's own decimals, which is typically 8. A target of 190.00 is submitted as 19000000000. Always call decimals() on the aggregator rather than assuming — the UI does this and so should any client.
Writing a call
import { parseUnits } from 'viem' import { client, robinhoodChain } from './client' const decimals = await client.readContract({ address: feed, abi: aggregatorAbi, functionName: 'decimals', }) const target = parseUnits('190.00', decimals) const deadline = BigInt(Math.floor(Date.parse('2026-09-25T21:00:00Z') / 1000)) const hash = await wallet.writeContract({ address: KIV, abi: kivAbi, functionName: 'makeCall', args: [feed, target, true, deadline], chain: robinhoodChain, }) const receipt = await client.waitForTransactionReceipt({ hash })
Pull the new ID out of the CallMade log rather than guessing it from callCount(), which can move between your read and your write.
Reading records
A profile is record(address) plus the author's logs. For a single wallet, filtering logs is enough; no indexer is needed until you are ranking thousands of addresses.
const [hits, misses, open] = await client.readContract({ address: KIV, abi: kivAbi, functionName: 'record', args: [who], }) const logs = await client.getContractEvents({ address: KIV, abi: kivAbi, eventName: 'CallMade', args: { author: who }, fromBlock: DEPLOY_BLOCK, }) const settled = hits + misses const rate = settled ? hits / settled : null // null, never 0 — an empty record is not a bad one
Display rule. Do not show a hit rate below five settled calls. Three out of three is noise, and presenting it as 100% turns a record into a marketing surface, which is the thing KIV exists to replace.
Errors
| Error | Cause | Fix |
|---|---|---|
TooEarly() | Deadline has not passed. | Wait. isSettleable tells you when. |
AlreadySettled() | Someone settled it first. | Read the Settled log; the result stands. |
StaleFeed() | Oracle has not updated since the deadline. | Retry later. The call stays open, not void. |
BadPrice() | Feed returned zero or negative. | Retry. Never settle off a fallback price. |
FeedNotAllowed() | Feed is not on the allow-list. | Use a listed asset. |
HorizonTooLong() | Deadline beyond MAX_HORIZON. | Shorten it. Long horizons outlive feeds. |
Known limits
- The allow-list is a centralisation point. KIV curates which feeds can be called, because an arbitrary address could be a fake aggregator that always returns a convenient price. Adding a feed is an admin action; changing a result is not possible at all.
- A record measures calls, not trading. Someone with a strong hit rate may never have taken the position. Nothing here implies profit.
- Selection bias is real. Authors choose which calls to publish, so a public record is the subset someone was willing to be judged on. Volume is shown next to hit rate for exactly this reason.
- Feeds can be deprecated. If an aggregator is retired before a deadline, affected calls may become permanently unsettleable.
MAX_HORIZONlimits the exposure but does not remove it. - Audit status is published on this page. Read the verified source on Blockscout before you interact with the contract.
Legal
KIV is an independent project built on Robinhood Chain. It is not affiliated with, endorsed by, sponsored by, or partnered with Robinhood Markets, Inc. or any of its subsidiaries. Robinhood Chain and related marks belong to their respective owners.
Nothing in this documentation or in the product is investment, legal or tax advice, and nothing here is a recommendation to buy or sell any asset. A published call records an opinion; it is not a forecast endorsed by KIV. Hit rates describe past calls only and say nothing about future results. Tokenized equities carry significant risk including total loss of capital and are unavailable in some jurisdictions. Smart contracts can contain defects.