Reading a Plugin feed safely
Five rules, copy-paste examples and the public JSON API. For tested Solidity templates, see FeedKit.
Five things to know first
- Always read the proxy (EACAggregatorProxy), never the aggregator directly: the owner can swap the aggregator behind it.
- Decimals differ per feed: most live feeds use 16 and some older ones 18 (and Plugin's contracts allow others, such as 25). Call
decimals(), don't hard-code 8. - Check
updatedAt. Many feeds update only on deviation or not at all (see the observed update gaps in the FeedKit feed table). Set a max age per feed. - Some proxies are access-controlled and revert with "No access" unless your contract is whitelisted. Ask Plugin through its feeds portal to add your contract to the feed's access list.
- Know who can change it: check
owner()on the proxy. Most feeds (60 of 90) share one owner; three other owners control the rest, including XDC/USDT. See the owner field in the feed list (JSON).
- Quote direction. A pair BASE/QUOTE reports the price of 1 BASE in QUOTE units, scaled by
decimals(). For example PLI/XDC reports how many XDC one PLI is worth, not the other way round. Feeds named USD/X (for example USD/AAVE) are inverted pairs. - Addresses. XDC explorers show addresses as
xdc…. This is display-only: on chain, in Solidity, viem, ethers and Foundry, use the same address with0x. Examples use checksummed0x. - Mainnet addresses. These feeds are on XDC mainnet (chainId 50). Plugin runs feeds on Apothem (51), but they are unlisted and less reliable: most of them stopped updating on 21 Oct 2026. Treat them as best-effort test data and check
updatedAtfrom latestRoundData; for reliable tests use a mainnet fork or a mock. Some of these mainnet addresses hold unrelated contracts on Apothem. - Reverts over JSON-RPC. Off chain (
eth_call), an access-controlled proxy returns a JSON-RPC error"execution reverted: No access"withError(string)data (0x08c379a0…). A broken proxy such as BTC/USDT0xEe28…bDCbreturns"execution reverted"with no data. Treat both as "feed unavailable", not as a transport failure, and don't retry them on another RPC. - Feed list as data. All 90 known proxies with pair, decimals, status, staleness default, owner and aggregator: /kit/feeds.json (static file, CORS-enabled, snapshot).
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IPluginFeed {
function decimals() external view returns (uint8);
function latestRoundData() external view returns (
uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}
contract UsesPluginPrice {
IPluginFeed public immutable feed = IPluginFeed(0x2baa1AEfa06D58454c9c06C56436CdD5B7904248); // PLI/USDT proxy
uint256 public constant MAX_AGE = 1 hours; // pick per feed: check its real update rhythm first
function price() external view returns (int256 answer, uint8 decimals) {
uint256 updatedAt;
(, answer, , updatedAt, ) = feed.latestRoundData();
require(answer > 0, "bad answer");
require(updatedAt != 0 && updatedAt <= block.timestamp, "bad updatedAt");
require(block.timestamp - updatedAt <= MAX_AGE, "stale price");
return (answer, feed.decimals());
}
}TypeScript (viem)
Save as read-feed.mjs (or .mts and run with npx tsx), because the example uses top-level await. Some public XDC RPCs sit behind Cloudflare and refuse requests from Node's default User-Agent with HTTP 403, so the example sends a User-Agent and falls back across RPCs. In the browser, leave out the header (browsers send their own) and start with https://erpc.xinfin.network.
// read-feed.mjs (ES module: top-level await). Run: node read-feed.mjs
import { createPublicClient, http, fallback, parseAbi, formatUnits } from "viem";
import { xdc } from "viem/chains";
// Some public XDC RPCs refuse Node's default User-Agent (HTTP 403). In a browser, drop the headers option.
const UA = { "User-Agent": "Mozilla/5.0 (compatible; feed-reader/1.0)" };
const rpcs = ["https://erpc.xinfin.network", "https://rpc.xinfin.network", "https://rpc.xdcrpc.com"];
const client = createPublicClient({
chain: xdc,
transport: fallback(rpcs.map((u) => http(u, { fetchOptions: { headers: UA }, retryCount: 1, timeout: 8000 }))),
});
const abi = parseAbi([
"function decimals() view returns (uint8)",
"function latestRoundData() view returns (uint80,int256,uint256,uint256,uint80)",
]);
const feed = "0x2baa1AEfa06D58454c9c06C56436CdD5B7904248"; // PLI/USDT proxy, XDC mainnet (chainId 50)
const MAX_AGE = 3600; // seconds: pick per feed (FeedKit default for PLI/USDT)
const [, answer, , updatedAt] = await client.readContract({ address: feed, abi, functionName: "latestRoundData" });
const decimals = await client.readContract({ address: feed, abi, functionName: "decimals" });
const { timestamp } = await client.getBlock();
const age = Number(timestamp - updatedAt);
if (answer <= 0n) throw new Error("bad answer");
console.log(formatUnits(answer, decimals), "decimals", decimals, "age", age, "s", age <= MAX_AGE ? "fresh" : "STALE");Public mainnet RPCs: https://erpc.xinfin.network, https://rpc.xinfin.network. https://rpc.xdcrpc.com answers without a key today, but XDC Gateway documents its endpoints as key-authenticated, so don't rely on it as a guaranteed keyless endpoint. The xinfin endpoints may return HTTP 403 to non-browser clients that send no User-Agent.
USD prices
Most crypto feeds quote USDT. For a USD value multiply by USDT/USD (0x5a792d09BAdeF90e599C13aff1552BF487Fe84B2) when both feeds are fresh, normalising decimals first: price18 = a18 * b18 / 1e18. For PLI: PLI/USD = PLI/USDT × USDT/USD. If you accept a stablecoin assumption (1 USDT = 1 USD), say so. As a sanity check, PLI/XDC × XDC/USD should agree with PLI/USDT × USDT/USD to within a few percent.
Public JSON API
The same snapshot this site uses, refreshed about every 5 minutes, CORS-enabled:
/api/feeds.json: all feeds with price, status, age, owner, aggregator/api/summary.json: counts only/api/history/<proxy-lowercase>.json: last 7 days of snapshots
curl -s https://feedplumb.com/api/feeds.json | jq '.feeds[] | select(.pair=="PLI/USDT")'Free and best effort, without an SLA. Please cache responses and poll at most once a minute.