개발자를 위한 문서 / 실물 자산
가격 읽기
네 개의 표면이 같은 질문에 답합니다. 보통 원하는 것은 그 피드의 어댑터입니다. Chainlink AggregatorV3Interface이고, 기존 코드가 이미 그 말을 할 줄 알기 때문입니다.
컨트랙트에서
The adapter implements AggregatorV3Interface with the same five functions and the same signatures, so existing code that reads a Chainlink feed compiles and runs against it unchanged. Point your consumer at the feed's adapter and it works.
두 가지 모양이 있고 같은 라운드를 보유합니다. 레지스트리는 모든 읽기를 feedId로 키잉하고, 피드별 어댑터는 하나의 feedId를 주소에 고정하므로 Chainlink를 상대로 쓰인 소비자는 피드 id에 대해 아무것도 알 필요가 없습니다.
// The adapter: what an existing Chainlink consumer already calls.
AggregatorV3Interface feed = AggregatorV3Interface(adapter);
(, int256 answer,, uint256 updatedAt,) = feed.latestRoundData();
uint8 decimals = feed.decimals(); // 8 on every feed today - read it anyway
// The registry: one address for all 33 feeds, keyed by the hash of the symbol.
bytes32 feedId = keccak256(bytes("XAU/USD"));
(, int256 gold,, uint256 goldUpdatedAt,) = registry.latestRoundData(feedId);
uint8 state = registry.status(feedId); // 1 open, 2 closed
uint64 observedAt = registry.latestObservedAt(feedId);Chainlink 소비자가 이것에 기대기 전에 알아야 할 여섯 가지입니다.
- One adapter per feed, holding no data: it stores the registry address and its own feedId and forwards every call. Rounds live in the registry.
- startedAt equals updatedAt. Every round is a single transmission, as an OCR feed reports it, and answeredInRound equals roundId because no round is ever carried over.
- updatedAt is the block timestamp of the publishing transaction, not the time the price was observed. The observation time is observedAt(feedId, roundId) on the registry, which usually LEADS updatedAt - that is why it is kept off the Chainlink tuple, where startedAt <= updatedAt is assumed.
- The adapter emits no events. A consumer watching AnswerUpdated on it sees nothing; the registry emits FeedUpdated(feedId, ...) instead, and feedId is indexed.
- version() returns 2, which is this layer's version and not a Chainlink aggregator version.
- latestRoundData() on a feed that has never been written reverts with Error("No data present"), bubbled from the registry unchanged. The legacy latestAnswer() does not revert - it reads the ring at index 0 and returns 0, which is exactly the zero a consumer must not treat as a price.
직접 검사하는 소비자는 어댑터보다 레지스트리를 원합니다. 상태와 관측 시각이 Chainlink 튜플에 없기 때문입니다.
interface IPickleFeeds {
function latestRoundData(bytes32 feedId)
external view returns (uint80, int256, uint256, uint256, uint80);
function status(bytes32 feedId) external view returns (uint8);
function decimals(bytes32 feedId) external view returns (uint8);
}
uint8 constant STATUS_OPEN = 1;
uint8 constant STATUS_CLOSED = 2;
/// maxAge is YOUR tolerance, in seconds, and it only applies while the market is open.
function priceOf(IPickleFeeds registry, bytes32 feedId, uint256 maxAge)
internal view returns (int256 answer, uint8 decimals)
{
uint256 updatedAt;
// Reverts with Error("No data present") when the feed has never been written.
(, answer,, updatedAt,) = registry.latestRoundData(feedId);
require(answer > 0, "feed: non-positive answer");
uint8 state = registry.status(feedId);
require(state == STATUS_OPEN || state == STATUS_CLOSED, "feed: status unknown");
// A closed market's last print is the right price for as long as it is closed.
if (state == STATUS_OPEN) {
require(block.timestamp - updatedAt <= maxAge, "feed: stale");
}
decimals = registry.decimals(feedId);
}레지스트리의 읽기 함수
- latestRoundData(bytes32 feedId) returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
- getRoundData(bytes32 feedId, uint80 roundId) returns (uint80, int256, uint256, uint256, uint80)
- latestAnswer(bytes32 feedId) returns (int256)
- latestTimestamp(bytes32 feedId) returns (uint256)
- latestRound(bytes32 feedId) returns (uint256)
- observedAt(bytes32 feedId, uint80 roundId) returns (uint64)
- latestObservedAt(bytes32 feedId) returns (uint64)
- status(bytes32 feedId) returns (uint8)
- source(bytes32 feedId) returns (uint8)
- decimals(bytes32 feedId) returns (uint8)
- description(bytes32 feedId) returns (string)
- symbol(bytes32 feedId) returns (string)
- feed(bytes32 feedId) returns (FeedView)
- feeds(uint256 offset, uint256 limit) returns (FeedView[])
- feedCount() returns (uint256)
- feedIdAt(uint256 index) returns (bytes32)
- adapterOf(bytes32 feedId) returns (address)
- adapterAddress(bytes32 feedId) returns (address)
- version() returns (uint256)
어댑터의 읽기 함수
- decimals() returns (uint8)
- description() returns (string)
- version() returns (uint256)
- latestRoundData() returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
- getRoundData(uint80 roundId) returns (uint80, int256, uint256, uint256, uint80)
- latestAnswer() returns (int256)
- latestTimestamp() returns (uint256)
- latestRound() returns (uint256)
- symbol() returns (string)
이 사이트에는 어느 주소도 찍혀 있지 않습니다. 주소 해석하기가 체인에서 그것을 얻는 방법입니다.
답과 그 소수 자리
답은 부동소수도 wei도 아닌 고정 소수점 정수입니다. 제네시스 집합의 모든 피드는 decimals = 8을 갖고 있으므로, 415012345678이라는 답은 4150.12345678입니다. 그 필드는 0에서 18까지 허용하고 피드마다 다르므로, 8을 소비자에 컴파일해 넣지 말고 피드에서 읽으십시오.
// answer arrives as a decimal string, because it does not fit a JS number safely.
const { answer, decimals, price } = result; // "415012345678", 8, "4150.12345678"
const scaled = BigInt(answer); // keep it integral
const human = Number(scaled) / 10 ** decimals; // only for display
// price is the same number pre-formatted with exactly `decimals` digits.
// Never parseFloat(answer) and never compare two feeds without rescaling both.0은 가격이 아닙니다
컨트랙트가 양수가 아닌 답을 거절하므로 0은 라운드에 닿지 않습니다. 그러니 읽은 0은 다른 데서 온 것입니다. 한 번도 기록된 적 없는 피드의 구식 latestAnswer()이거나, 코드가 없는 주소로의 호출입니다. 뒤의 것은 성공하면서 아무것도 돌려주지 않습니다. roundId != 0을 확인하거나, 대신 revert하는 latestRoundData()를 쓰십시오.
JSON-RPC로
세 개의 메서드이며, 전부 키도 계정도 필요 없는 평범한 읽기입니다. 다른 모든 메서드와 함께 JSON-RPC 레퍼런스에 실려 있습니다. 아래는 그것들이 이 레이어에 대해 답하는 내용입니다.
- Params
- none
- 반환
- { core, version, blockNumber, timestamp, publisher, owner, genesisStateHash, feeds: [FeedObject] }
The whole registry in one read, and the answer to "what assets are there". The node reads feeds(offset, 256) against the published state snapshot once per EVM block and memoises it, so a client polling the list costs about what one eth_call costs.
- Params
- [feed] - "ETH/USD", "ETH-USD", "eth-usd" or "0x<64 hex feedId>"
- 반환
- one FeedObject, the shape pickle_prices lists
One feed. A name that is not a well-formed symbol or a 32-byte id, or one the chain does not carry, answers -32602 "unknown feed" - never an empty answer that reads as a price of zero.
- Params
- none
- 반환
- { configured, pair, contract, roundId, answer, price, decimals, updatedAt, observedAt, ageSecs, staleAfterSecs, staleOnchain, feedId, adapter, core }
ETH/USD alone, in the shape the first version of this layer served, with contract set to the feed's adapter. When the feed cannot be read it answers an object carrying an error field rather than failing, so a caller must check for it.
curl -s $RPC_URL -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"pickle_price","params":["AAPL-USD"]}'
# -> {"jsonrpc":"2.0","id":1,"result":{
# "symbol":"AAPL/USD","feedId":"0x...","adapter":"0x...","decimals":8,
# "roundId":184,"answer":"23144000000","price":"231.44000000",
# "updatedAt":1758470400,"observedAt":1758470396,
# "status":2,"statusName":"closed","source":2,"sourceName":"pyth",
# "ageSecs":51840,"staleAfterSecs":330,"staleOnchain":false, ... }}
#
# staleOnchain is FALSE at fourteen hours old, because the market is closed.피드 객체
| 필드 | Value |
|---|---|
| symbol | "ETH/USD"the canonical spelling |
| feedId | 0x + 64 hexkeccak256 of the symbol |
| adapter | checksummed addressthis feed's Chainlink-shaped face; the one to point a consumer at |
| decimals | 8read it rather than assuming it |
| roundId | uint0 means never written. answer, price, updatedAt and observedAt are then null |
| answer | integer as a stringthe fixed-point integer. 415012345678 at 8 decimals is 4150.12345678 |
| price | decimal stringthe same number formatted with exactly `decimals` fractional digits. A convenience, never a float |
| updatedAt | unix secondsthe timestamp of the block that wrote the round |
| observedAt | unix secondswhen the source saw the price. Usually LEADS updatedAt |
| status / statusName | 0 unknown, 1 open, 2 closed, 3 halted3 is reserved and never published. Check this before you check the age |
| source / sourceName | 0 unknown, 1 exchanges-median, 2 pythwhich method produced this round |
| ageSecs | now - updatedAtwall clock at the request, not at the seal. null on an unwritten feed |
| staleAfterSecs | 30, or heartbeat + 30flat on an everyBlock feed; a deviation feed gets its heartbeat plus the default, so a feed that has not moved is not stale until it has missed a heartbeat |
| staleOnchain | booleanageSecs > staleAfterSecs AND the status is not closed. An unwritten feed is always true |
| policy / policyName | 0 paused, 1 everyBlock, 2 deviationwith deviationBps, heartbeatSecs and maxStepBps beside them |
| capacity | ring lengthhow many rounds back getRoundData can still reach |
All three methods run on the blocking pool and draw on the same token bucket as eth_call, eth_estimateGas and eth_getLogs: they simulate against the state snapshot. Exhausting it answers -32005 "rate limit exceeded for expensive read methods".
The registry read is memoised on the block tip, so the first caller in a block pays for the catalog decode and the rest do not. Leader and replica answer identically because both read the published snapshot rather than live execution state.
오라클 서비스에서
이 API는 공개 호스트에 발행되어 있지 않습니다
The oracle service answers on port 3005 for reads and 3006 for the batch the node polls. The deployment binds 3005 to loopback and leaves 3006 unpublished, and no public vhost proxies either one.
Treat the JSON-RPC price methods as the public read surface. The routes below matter when you run the stack yourself, and when you need the per-venue detail the chain does not carry.
그것이 체인에 더해 주는 것은 풀이 과정입니다. 어느 거래소가 신선했는지, 어느 것이 중앙값에 포함되었는지, 각각이 거기서 얼마나 떨어져 있었는지, 그리고 피드가 왜 발표하지 않는지입니다. 가격이 없을 때 그 이유가 있는 곳이 여기입니다.
/v1/prices
GETEvery feed's FeedView, with generatedAtMs. The service-side equivalent of pickle_prices, plus the per-source detail: which venue was fresh, which was included in the median, each one's age, spread and delivery lag.
/v1/price/{symbol}
GETOne feed. The symbol may be ETH/USD, ETH-USD, eth-usd, the percent-encoded slash, or 0x + 64 hex. Anything else, or a feed that does not exist, answers 404 {"error":"unknown feed"}.
/v1/stream
GETServer-sent events. An immediate prices event with every feed, then one per change carrying only the feeds that changed, plus an onchain event per new block listing the feeds whose round advanced. Bursts are coalesced and a keepalive comment is sent when nothing has moved.
/v1/eth-usd and /v1/eth-usd/stream
GETThe ETH/USD view and its stream in the shape the first version of this service served, kept for its consumers. The same data as /v1/price/ETH-USD under the original key names.
/v1/sources
GETOperational rather than price data: every venue with its connection state, per-product message counts, reconnects and last message time, the Pyth client's state and per-feed accept and reject counters, and the chain poller's own status. This is where you look when a feed is stale and you want to know which leg failed.
/v1/contract
GETThe registry's address, owner, publisher and feed count as read from the chain, the two ABIs when the build artefacts are present, every feed's adapter, and the event topic hashes - recomputed locally so they are served even without the ABI files.
/v1/pyth/audit/{symbol}
GETThe raw accepted updates for one feed, up to 256 deep, with their publish times and slots, plus the reject counters by reason. The audit trail behind a price.
/health and /ready
GET/health is 200 unless a worker thread has died. /ready is 503 while starting, when no feed is fresh, when the chain is unreachable or the registry has gone stale on chain - and 200 with a pyth_unauthorized warning when the credential is missing, because that is a degraded service rather than an unready one.
포트 둘, 청중 둘입니다. 읽기 API는 3005에서 답하고, 노드가 폴링하는 배치는 3006의 별도 리스너이며 본문 바이트 그대로의 HMAC을 X-Pickle-Batch-Hmac에 실어 나릅니다. 공개 리스너에 /v1/publish를 물으면 그 라우트가 있다고 인정하는 대신 404로 답합니다. 모든 오류 본문은 고정된 문자열입니다. 경로도, 상류도, 예외 텍스트도 없습니다.
피드 뷰가 체인 너머로 담는 것
| 필드 | Value |
|---|---|
| stale + reason | boolean and a codeno_sources, too_few_sources, starting, market_closed, cross_check_diverged, or a Pyth reason - state, age, lag, no_data |
| market | { isOpen, schedule, hermesIsOpen, nextOpen, nextClose, nextTransition }present on all 33 feeds, since every one carries a Pyth id; null only on a feed configured without one. The schedule is the raw schedule string |
| sources | one row per venue, plus the Pyth rowprice, kind (mid or last), observedAtMs, ageMs, fresh, included, spreadBps, lagMs, state |
| degraded | booleanthe median was taken on exactly minSources venues; one more drop-out and the feed stops publishing |
| dispersed + dispersionBps | boolean and an integerthe spread between the included venues, and whether it exceeded the limit even after the outlier was dropped |
| outlierDropped | venue name or nullwhich venue was excluded from this median |
| crossCheckBps + crossCheckDiverged | integer and booleanhow far the exchange median sits from the Pyth price for the same feed |
| lastKnown | { price, observedAtMs, ageMs } or nullthe last good answer while the feed is stale. Shown for diagnosis - it is not republished on chain |
| onchain | the chain's own row for this feedpolled once a second, so the service can show what actually landed beside what it computed |
신선도
나이보다 상태를 먼저 읽으십시오
Read status first, then staleness. A closed market's last print is the correct price for as long as the market is closed, and a naive "reject anything older than N minutes" check rejects every equity every weekend.
| 경우 | 규칙 |
|---|---|
| everyBlock feed | stale after 30 sORACLE_STALE_AFTER_SECS on the node, matching the service's setting of the same name |
| deviation feed | stale after heartbeat + 30 s90 s on the crypto feeds, 330 s on every real-world feed. A feed that has not moved is not late |
| status closed | never stale by agestaleOnchain stays false while the market is closed, however old the round is |
| roundId 0 | always stalenever written. answer and price are null, and the adapter's latestRoundData reverts |
staleOnchain은 노드가 그 규칙들을 여러분 대신 적용한 것이며, 봉인 시각이 아니라 요청 시각의 벽시계를 씁니다. 편의 기능입니다. 컨트랙트 안의 소비자에게는 그런 필드가 없으므로 updatedAt을 자기 허용치와 비교해야 하며, 그것이 위의 예제가 하는 검사입니다.
모든 경우에 정직한 신호는 updatedAt입니다. 발표가 멈추면 그저 나아가기를 멈추기 때문입니다. 기댈 만한 온체인 나이 검사는 없습니다. There is deliberately no on-chain age check on observedAt. The node replays its write-ahead log with the restart-time timestamp, so a rule of the form block.timestamp - observedAt <= maxAge would fail every replayed update. Freshness is enforced by the publisher, which does not submit a stale aggregate, and by you, through updatedAt - which simply stops advancing when publication stops.
서비스는 stale과 함께 reason을 보고하며, 그 이유들은 따로 다룰 값어치가 있습니다. too_few_sources는 거래소가 빠졌다는 뜻이고, cross_check_diverged는 거래소들과 두 번째 소스가 어긋나서 피드가 일부러 침묵한다는 뜻이며, market_closed는 아무 문제도 없다는 뜻입니다. 각각이 어떻게 생기는지는 가격이 만들어지는 방식에 있습니다.
시장 개장 시간
Each feed with a Pyth leg carries a schedule: a time zone, seven day patterns starting Monday, and a list of dated overrides for holidays and half-days. It is resolved against the real time-zone database, so a market opens at 09:30 local whatever the daylight-saving offset that day.
| 구역 | 문법 |
|---|---|
| Time zone | America/New_Yorkthe first section; every schedule in the catalog uses it |
| A day | O, C, or HHMM-HHMMO is the whole day, C is closed. Ranges are half-open and joined with & for a day with a break |
| Overrides | MMDD/speca dated exception using the same day grammar - 1225/C for a holiday, 1127/0930-1300 for a half-day |
| 피드 | 일정 |
|---|---|
| Crypto and the energy indices | O,O,O,O,O,O,Oopen every day, all day, with no overrides |
| The nine equities | 0930-1600 Monday to Friday, C,C at the weekendwith the exchange holiday and half-day list as overrides |
| EUR/USD, GBP/USD, USD/JPY, USD/CHF | continuous to 17:00 Friday, closed Saturday, reopening 17:00 Sundaytwo dated overrides shorten the sessions around the end of December |
| XAU/USD, XAG/USD, USD/CNH | a daily 17:00-18:00 break, closed Saturday, reopening Sunday eveningthe metals carry the longer holiday list |
The upstream also reports whether the market is open right now. That flag wins while its poll is fresh and no scheduled transition has passed since - it knows about a halt or an unlisted holiday that the schedule does not. Otherwise the schedule decides.
The status machinery applies to a feed whose price comes from the second source, which is every real-world feed. A feed publishing an exchange median reports status open, and that is consistent rather than a gap: all thirteen of them are crypto pairs whose schedule is open every day, all day.
닫힌 시장은 오래된 피드가 아닙니다
The price that stands while a market is closed is the one seen at the close, never anything republished afterwards. It is published with status closed and reason market_closed, and it is republished on a one-hour heartbeat rather than on the feed's normal cadence.
재개장 게이트. Going from closed back to open needs one accepted update whose publish time is later than the newest one seen up to the reopen. Until that arrives the feed keeps reporting closed and the standing price, so a stale pre-close print is never labelled as an open-market quote. A restart while the market is closed adopts the first accepted update as the standing price, because there was no close to observe.
갭은 예상된 것입니다. The first print after a reopen legitimately gaps. The contract's step check is bypassed whenever the status changed, so that print lands instead of being skipped as an implausible move.
소비자에게 그것은 세 가지를 뜻합니다. 주식 피드는 오래된 것이 아니면서도 이틀 반 동안 정당하게 변하지 않을 수 있습니다. 순진한 신선도 검사는 주말마다 모든 실물 자산 피드를, 그리고 밤마다 그 하나하나를 거부합니다. 그리고 지금 아무도 만들고 있지 않은 가격으로 청산되어서는 안 되는 포지션이라면 status를 읽고 그것이 2인 동안에는 움직이기를 거부해야 합니다. 옳지만 멈춰 있는 그대로의 가격으로 움직이는 대신에 말입니다.
const feed = result; // one feed object
if (feed.roundId === 0) throw new Error("never written");
if (feed.statusName === "unknown") throw new Error("status not established");
const closed = feed.statusName === "closed";
if (!closed && feed.staleOnchain) throw new Error("stale while open");
// closed === true is a valid, current price for a market that is not trading.
// Decide deliberately whether your product acts on it.라운드, 히스토리, 건너뜀
Rounds live in a fixed-size ring per feed rather than an append-only history. Round r sits at index r % capacity, is readable while latestRound - r < capacity, and is overwritten afterwards.
getRoundData on an evicted round reverts with "No data present", not with a zero answer. A feed with capacity 1,024 keeps its last 1,024 rounds and no more, so an indexer that wants full history reads the FeedUpdated logs rather than paging the ring.
updateMany never reverts on a bad item. One stale or out-of-range feed in a batch of sixty must not cost the other fifty-nine their round, so a bad item is skipped with a FeedSkipped(feedId, reason) log and the rest are written. Only a malformed batch - mismatched array lengths, over maxBatch, or a sender that is not the publisher - reverts.
| 이유 | 의미 |
|---|---|
| 1 | unknown feedno feed registered under that id |
| 2 | pausedthe feed's policy is paused |
| 3 | answer out of rangenot strictly positive, or wider than int128 |
| 4 | observedAt in the futurebeyond block.timestamp + futureTolerance |
| 5 | observedAt regressedolder than the feed's last observation; the round would go backwards in time |
| 6 | step too largea move wider than the feed's maxStepBps - 2,000 bps on the equities and energy indices, 1,000 on the metals, 500 on FX, disabled on everything crypto. Bypassed whenever the status changed, so a reopening gap still lands |
건너뛴 항목은 FeedSkipped 로그로만 보입니다. 라운드는 나아가지 않고 이전 답이 그대로 섭니다. 그러므로 피드 하나를 지켜보는 소비자는 피드 id를 인덱싱된 토픽으로 삼아 FeedUpdated를 지켜보아야 하며 - 어댑터가 발행하지 않는 이벤트입니다 - 긴 침묵은 결코 오지 않을 오류를 기다리는 대신 있는 그대로 받아들여야 합니다.