Documentation

The inference index

The inference index is the price of intelligence, written on-chain. Once an hour a Chainlink Functions network reads OpenRouter's public list of model prices, works out what the top frontier models and the top open-weight models charge per million tokens, and stores the answer in the InferenceIndex contract. The contract keeps a week of those hourly samples and publishes their median, which is where INT, OPEN and AGENT get their prices. Nobody edits it: the code the network runs and the lists of models it looks at both live on-chain.

The four numbers

Every sample is four values, each in USD × 1e18 per one million tokens:

Metric Index Meaning
inFrontier 0 median input (prompt) price across the frontier class
outFrontier 1 median output (completion) price across the frontier class
inOpen 2 median input price across the open-weight class
outOpen 3 median output price across the open-weight class

The three coin prices are derived from them at publish time:

Coin Derivation In the contract
INT outFrontier median(1)
OPEN 1000 × outOpen median(3) × 1000
AGENT 1.6 × inFrontier + 0.4 × outFrontier (median(0) × 16 + median(1) × 4) / 10

How a sample is made

request() sends a Chainlink Functions request carrying the JavaScript stored in the contract's config and two arguments: the frontier model list and the open-weight model list, both as comma-separated OpenRouter model ids. The source is contracts/functions/inference-index.js and it is short enough to read in full.

The Functions network (the DON) runs it. The script fetches https://openrouter.ai/api/v1/models, keeps only the models named in the two lists, reads each one's listed prompt and completion price, and takes the equal-weighted median per class and per side. It converts dollars per token to dollars per million tokens in 1e18 fixed point and returns the four values ABI-encoded as a uint256[4].

Two things make the script fail on purpose. If OpenRouter is unreachable or answers with something other than a model list, it throws. If fewer than five models in a class have a positive listed price, it throws class too small. A thrown error reaches the contract as an error response, which is recorded and ignored; the feed is not touched.

Storing and publishing

The DON's callback, handleOracleFulfillment, runs under a 250,000 gas cap and does one thing: it stores the sample. It accepts only the most recent request id. If the response carries an error, is not exactly four words long, or contains a value that is zero or above MAX_SAMPLE (1e24, which is $1,000,000 per million tokens), it emits RequestFailed and stores nothing. Otherwise it writes the four values into a 168-slot ring, one slot per metric per hour, and emits SampleStored.

publish() is a separate call anyone can make once a new sample has been stored since the last publish, and once at least 24 samples are in the window — a day of hourly readings — so the very first price the index ever publishes is already a day's median, not a single reading. It sorts each metric's stored samples, takes the median, derives the three coin prices from the table above, and pushes them to the PriceFeed under keccak256("INT"), keccak256("OPEN") and keccak256("AGENT"). The InferenceIndex is the only address the feed accepts pushes from, and it can only push what the median says. publish reverts with NotEnoughSamples during that first day and with NothingToPublish when there is nothing new.

Published class prices carry a six-hour staleness window on the feed. If nobody has published for six hours — the DON has been failing, or nobody called request and publish — the three coins read as stale, launches paired with them revert, reprice reverts, and redemptions halt until a fresh publish lands. Everything else keeps working.

What a single sample cannot do

The published price is the median of up to 168 hourly samples, so one wrong sample does not move it at all. If a provider mislists a price for an hour, or the DON returns one strange reading, that sample sits at the edge of the sorted window and the median stays where it was.

The same property sets the speed of the index. When the real price of inference steps to a new level, the median only follows once more than half the window has been sampled at the new level. That takes 84 hours, or three and a half days. Until then the published price moves gradually from the old level toward the new one as the window turns. A price cut announced today reaches INT in full on the fourth day.

Before the ring is full the median is taken over however many samples exist, so the index is coarser in its first week than after it.

The model lists

A class is a list of OpenRouter model ids. The rule at launch: the ten highest-scoring models on the Artificial Analysis Intelligence Index that have a listed output price on OpenRouter form the frontier class, and the ten highest-scoring open-weight models form the open-weight class. The lists are stored in the contract's config as frontierIds and openIds, readable by anyone, and the script only prices models that are on them.

Changing a list, the source, the subscription or the DON id is a two-step owner action. proposeConfig(newConfig) records the proposal and its hash and emits ConfigProposed with the time it can apply. applyConfig() succeeds only after CONFIG_DELAY, which is 24 hours, and only for the owner. In between, pendingConfig() returns the whole proposed config, so anyone can read what is about to change and act on it before it does. A proposal can be replaced by a new one, which restarts the delay. A config with an empty source or an empty list is rejected.

A list change does not rewrite history: the ring keeps its stored samples, and the median blends the old class into the new one over the same three and a half days.

Who calls it, and who pays

request() is permissionless and rate-limited: the contract refuses a request less than 55 minutes after the previous one, so at most about 26 requests a day are possible whoever calls. The caller pays the transaction's gas. The DON's work is billed in LINK to the Chainlink Functions subscription the contract is registered under, which the protocol keeps funded. publish() is permissionless and costs its caller a sort of up to 168 values per metric plus the push.

Nobody has to be the one who calls. The coins page shows a Publish button whenever lastSampleAt is later than lastPublishedAt, a cron anyone runs can call request hourly and publish after it, and Chainlink Automation can be pointed at both. Automation covers the rest of the calls that work this way.

The other price sources

The PriceFeed holds three kinds of asset. Only the three class prices are pushed. Everything else is read from the chain at the moment a contract asks.

Asset Source How it is read Stale when
NVDAc, GOOGLc, METAc, MSFTc, AMZNc, SNDKc that stock's Chainlink aggregator latestRoundData() at call time, scaled from 8 to 18 decimals the round is older than 26 hours, or the answer is not positive
ETH Chainlink ETH/USD the same the round is older than 2 hours
VVV the Aerodrome VVV/WETH pool quote(VVV, 1e18, 12), a time-weighted average over the last 12 half-hour observations, about six hours, multiplied by the Chainlink ETH/USD price the pool returns zero, or ETH/USD is stale
DIEM the Aerodrome VVV/DIEM pool quote(DIEM, 1e18, 12), the same six-hour average in VVV, multiplied by the VVV price above the pool returns zero, or anything under it is stale

The Aerodrome quote is a cumulative-price average maintained by the pool itself, so the DIEM price a contract sees is not a spot price anyone can move inside one block; moving it by 1% for six hours costs real money on a real pool. DIEM therefore reads as DIEM → VVV → WETH → USD, three on-chain steps, with the deepest VVV venue (against WETH, about $14M of liquidity) doing the heavy lifting. On 2026-09-16 that chain read about 79 VVV per DIEM and about $22 per VVV, so around $1,700 per DIEM.

A stale answer at any step reverts getPrice with Stale(). The vault's coverageBps and every needs* view read prices without reverting, so a dead source shows up as a zero component or a false flag rather than a broken page. The addresses of every source are in SPEC.md §1 and in Contracts.

Reading it

Read What it tells you
median(metric) the current median for metric 0–3 over the stored window
latestSample() the last sample stored, all four values
count, head how many samples are in the ring (up to 168) and the next slot to be written
lastRequestAt, lastSampleAt, lastPublishedAt timestamps of the last request, the last stored sample and the last publish
config(), pendingConfig(), pendingApplyAfter the live config, the proposed one, and when it may apply
PriceFeed.getPrice(keccak256("INT")) the published price and when it was pushed

Events: Requested, SampleStored, RequestFailed, Published, ConfigProposed, ConfigApplied. The coins page draws the 168-sample window, the median, the last sample and the two model lists from these reads.