From 71d8eb874b32f715698bc567b1e050d4802e3ba8 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 13:28:40 -0700 Subject: [PATCH 01/28] add agent-friendly data product table look up --- public/llms.txt | 32 +- src/content/data-feeds/llms.txt | 41 ++- src/content/data-streams/llms.txt | 39 ++- src/features/feeds/utils/feedOutput.ts | 326 ++++++++++++++++++ src/features/feeds/utils/streamMetadata.ts | 130 +++++++ src/pages/[...path].md.ts | 274 +++++++-------- .../data-feeds/feed-addresses/[type].txt.ts | 95 +++++ .../feed-addresses/[type]/[network].txt.ts | 74 ++++ src/pages/data-streams/networks.txt.ts | 105 ++++++ .../data-streams/stream-ids/[type].txt.ts | 113 ++++++ 10 files changed, 1059 insertions(+), 170 deletions(-) create mode 100644 src/features/feeds/utils/feedOutput.ts create mode 100644 src/features/feeds/utils/streamMetadata.ts create mode 100644 src/pages/data-feeds/feed-addresses/[type].txt.ts create mode 100644 src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts create mode 100644 src/pages/data-streams/networks.txt.ts create mode 100644 src/pages/data-streams/stream-ids/[type].txt.ts diff --git a/public/llms.txt b/public/llms.txt index c332e5e2f23..baab91f1831 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -199,17 +199,35 @@ Use these for deeper product-level navigation. These indexes provide broader cov - [DTA Technical Standard Documentation Index](https://docs.chain.link/dta-technical-standard/llms.txt) - [VRF Documentation Index](https://docs.chain.link/vrf/llms.txt) -## Live Data and Directories +## Live Data and Structured Datasets -Use these only when retrieving current network data, addresses, or routing information. +Use structured datasets when retrieving current network data, addresses, or routing information. -If `.md` is unavailable or incomplete, use the HTML page as the source of truth. Do not infer or reconstruct missing values. +Do not scrape documentation pages or interactive tables for structured data. -- [CCIP Mainnet Directory](https://docs.chain.link/ccip/directory/mainnet) -- [CCIP Testnet Directory](https://docs.chain.link/ccip/directory/testnet) -- [Price Feed Addresses](https://docs.chain.link/data-feeds/price-feeds/addresses) -- [Cryptocurrency Streams](https://docs.chain.link/data-streams/crypto-streams) +### Data Feeds +- Index (select network): + https://docs.chain.link/data-feeds/feed-addresses/default.txt + +- Per-network datasets: + https://docs.chain.link/data-feeds/feed-addresses/{type}/{network}.txt + +### Data Streams + +- Stream ID datasets: + https://docs.chain.link/data-streams/stream-ids/crypto.txt + +- Network metadata (verifier proxies): + https://docs.chain.link/data-streams/networks.txt + +### CCIP Directory + +- CCIP Mainnet Directory: + https://docs.chain.link/ccip/directory/mainnet + +- CCIP Testnet Directory: + https://docs.chain.link/ccip/directory/testnet ## Full Documentation Bundles diff --git a/src/content/data-feeds/llms.txt b/src/content/data-feeds/llms.txt index a38fdfa0397..189c85d9471 100644 --- a/src/content/data-feeds/llms.txt +++ b/src/content/data-feeds/llms.txt @@ -4,7 +4,7 @@ Prefer specific `.md` pages. Do not rely on memorized feed addresses. -Use address pages only when retrieving current feed addresses. +Use address datasets only when retrieving current feed addresses. ## Overview and Setup @@ -38,13 +38,36 @@ Use address pages only when retrieving current feed addresses. - [Contract Registry](https://docs.chain.link/data-feeds/contract-registry.md) - [Historical Data](https://docs.chain.link/data-feeds/historical-data.md) -## Live Data +## Feed Address Retrieval -Use only when retrieving current addresses. If `.md` output is incomplete, use the HTML page as the source of truth. +Use structured datasets when retrieving feed addresses. Documentation pages may contain partial or example data only. -- [Price Feed Addresses](https://docs.chain.link/data-feeds/price-feeds) -- [Rates Feed Addresses](https://docs.chain.link/data-feeds/rates-feeds/addresses) -- [SmartData Addresses](https://docs.chain.link/data-feeds/smartdata/addresses) -- [U.S. Government Macroeconomic Data Feeds](https://docs.chain.link/data-feeds/us-government-macroeconomic/addresses) -- [Rate and Volatility Feeds](https://docs.chain.link/data-feeds/rates-feeds/addresses) -- [Deprecating Feeds](https://docs.chain.link/data-feeds/deprecating-feeds) +Start with the dataset index: + +- Standard price feeds: https://docs.chain.link/data-feeds/feed-addresses/default.txt +- Rates feeds: https://docs.chain.link/data-feeds/feed-addresses/rates.txt +- SmartData feeds: https://docs.chain.link/data-feeds/feed-addresses/smartdata.txt +- U.S. Government Macroeconomic Data Feeds: https://docs.chain.link/data-feeds/feed-addresses/usGovernmentMacroeconomicData.txt +- Tokenized equity feeds: https://docs.chain.link/data-feeds/feed-addresses/tokenizedEquity.txt + +Each index lists available networks. Each network is identified by its `queryString`. + +Retrieve a specific dataset: + +- https://docs.chain.link/data-feeds/feed-addresses/{type}/{network}.txt + +Usage pattern: + +1. Open the index file for the desired feed type +2. Identify the target network `queryString` +3. Retrieve the corresponding dataset +4. Filter by feed name as needed + +Constraints: + +- Each network dataset contains the complete feed list for that network +- Datasets are structured and can be parsed programmatically +- Loading all networks will significantly increase response size and latency +- Do not load multiple networks unless required +- Do not scrape documentation pages or interactive tables for feed addresses +- Do not rely on memorized or cached addresses \ No newline at end of file diff --git a/src/content/data-streams/llms.txt b/src/content/data-streams/llms.txt index eabc9d5513c..b89432197b9 100644 --- a/src/content/data-streams/llms.txt +++ b/src/content/data-streams/llms.txt @@ -17,7 +17,7 @@ Do not rely on memorized supported networks, stream IDs, verifier addresses, or Use these pages to choose the correct stream category and report schema before implementation. -These pages may include data-driven or dynamically rendered content. If a `.md` page is incomplete or unavailable, use the corresponding HTML page as the source of truth. Do not infer stream IDs, schemas, supported networks, or verifier details. +These pages may include data-driven or dynamically rendered content. Do not infer stream IDs, schemas, supported networks, or verifier details from documentation pages. - [Stream Categories and Report Schemas](https://docs.chain.link/data-streams/reference/report-schema-overview.md): Start here to choose the correct category and schema. @@ -42,6 +42,41 @@ Additional Data and Lifecycle - [Fetch Reports with Rust SDK](https://docs.chain.link/data-streams/tutorials/rust-sdk-fetch.md) - [Stream Reports with Rust SDK](https://docs.chain.link/data-streams/tutorials/rust-sdk-stream.md) +## Stream ID Retrieval + +Use structured datasets to retrieve stream IDs and network metadata. + +Start with the dataset for the desired category: + +- Crypto streams: https://docs.chain.link/data-streams/stream-ids/crypto.txt +- RWA streams: https://docs.chain.link/data-streams/stream-ids/rwa.txt +- SmartData streams: https://docs.chain.link/data-streams/stream-ids/smartdata.txt +- Exchange rate streams: https://docs.chain.link/data-streams/stream-ids/exchangeRate.txt +- Tokenized asset streams: https://docs.chain.link/data-streams/stream-ids/tokenizedAsset.txt + +Stream IDs are universal and valid across all supported networks. + +To retrieve verifier proxy addresses: + +- https://docs.chain.link/data-streams/networks.txt + +## Retrieval Pattern + +1. Select the appropriate stream category +2. Retrieve the dataset for that category +3. Identify the required stream ID +4. Retrieve network metadata from `/data-streams/networks.txt` +5. You must use the verifier proxy for the network where your application is deployed + +## Constraints + +- Stream IDs are not network-specific +- Datasets may contain multiple schema versions +- Datasets are structured and can be parsed programmatically +- Do not duplicate requests per network +- Do not scrape documentation pages or interactive tables +- Do not rely on memorized or cached values + ## Operations and Billing - [Billing](https://docs.chain.link/data-streams/billing.md) @@ -49,4 +84,4 @@ Additional Data and Lifecycle ## Full Context -- [Data Streams Full](https://docs.chain.link/data-streams/llms-full.txt) +- [Data Streams Full](https://docs.chain.link/data-streams/llms-full.txt) \ No newline at end of file diff --git a/src/features/feeds/utils/feedOutput.ts b/src/features/feeds/utils/feedOutput.ts new file mode 100644 index 00000000000..4c8c91f0ff0 --- /dev/null +++ b/src/features/feeds/utils/feedOutput.ts @@ -0,0 +1,326 @@ +import { CHAINS } from "~/features/data/chains.ts" +import { isFeedVisible } from "./feedVisibility.ts" +import { + resolveStreamPair, + resolveAssetClass, + resolveTradingHours, + resolveStreamSchema, + escapePipes, +} from "./streamMetadata.ts" +import type { DataFeedType } from "../components/FeedList.tsx" + +export const VALID_FEED_TYPES: DataFeedType[] = [ + "default", + "smartdata", + "rates", + "streamsCrypto", + "streamsRwa", + "streamsNav", + "streamsExRate", + "streamsBacked", + "tokenizedEquity", + "usGovernmentMacroeconomicData", +] + +export const FEED_TYPE_LABELS: Record = { + default: "Standard Price Feeds", + smartdata: "SmartData Feeds (Proof of Reserve, NAVLink, SmartAUM)", + rates: "Rates Feeds", + streamsCrypto: "Data Streams — Crypto", + streamsRwa: "Data Streams — RWA", + streamsNav: "Data Streams — SmartData (NAV)", + streamsExRate: "Data Streams — Exchange Rate", + streamsBacked: "Data Streams — Tokenized Assets", + tokenizedEquity: "Tokenized Equity Feeds", + usGovernmentMacroeconomicData: "US Government Macroeconomic Data Feeds", +} + +export function isStreamsType(type: DataFeedType): boolean { + return ( + type === "streamsCrypto" || + type === "streamsRwa" || + type === "streamsNav" || + type === "streamsExRate" || + type === "streamsBacked" + ) +} + +export function formatHeartbeat(seconds: number): string { + if (!seconds) return "N/A" + if (seconds < 60) return `${seconds}s` + if (seconds < 3600) return `${Math.round(seconds / 60)}m` + return `${Math.round(seconds / 3600)}h` +} + +export function formatDeviation(threshold: number): string { + if (threshold == null || threshold === 0) return "N/A" + return `${threshold}%` +} + +export interface FeedMarkdownOptions { + /** Filter by schema version, e.g. "v8" or "v11". Only meaningful for streamsRwa. */ + schemaFilter?: string + /** Public-facing API type name for use in generated URLs (e.g. "crypto" instead of "streamsCrypto"). */ + publicType?: string + /** Restrict to "mainnet" or "testnet" networks only. */ + networkType?: "mainnet" | "testnet" +} + +// --------------------------------------------------------------------------- +// Structured data types — used for JSON output +// --------------------------------------------------------------------------- + +export interface StreamEntry { + name: string + feedId: string + assetClass: string + schema: string + tradingHours?: string +} + +export interface FeedEntry { + name: string + proxyAddress: string + deviation: string + heartbeat: string + network: string + networkName: string + networkType: string + /** Blockchain ecosystem name (e.g. "Ethereum", "Arbitrum"). */ + chain: string + svr?: "shared" | "aave" +} + +// --------------------------------------------------------------------------- +// Shared data collectors — used by both markdown and JSON formatters +// --------------------------------------------------------------------------- + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function resolveSVR(feed: any): "shared" | "aave" | undefined { + const isShared = typeof feed.path === "string" && /-shared-svr$/.test(feed.path) + if (isShared) return "shared" + if (feed.secondaryProxyAddress) return "aave" + return undefined +} + +export function collectStreamEntries( + type: DataFeedType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + chainCache: Record, + options: FeedMarkdownOptions = {} +): StreamEntry[] { + const visibilityOpts = { schemaFilter: options.schemaFilter } + const seenFeedIds = new Set() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const raw: any[] = [] + + for (const chain of CHAINS) { + const chainData = chainCache[chain.page] + if (!chainData?.networks) continue + // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const network of chainData.networks as any[]) { + if (!network?.metadata?.length) continue + if (network.networkType !== "mainnet") continue + // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const feed of network.metadata as any[]) { + if (!isFeedVisible(feed, type, "", visibilityOpts)) continue + if (!feed.feedId || seenFeedIds.has(feed.feedId)) continue + seenFeedIds.add(feed.feedId) + raw.push(feed) + } + } + } + + raw.sort((a, b) => (resolveStreamPair(a) ?? "").localeCompare(resolveStreamPair(b) ?? "")) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return raw.flatMap((feed: any) => { + const name = resolveStreamPair(feed) + if (!name) return [] + const tradingHours = resolveTradingHours(feed) + const entry: StreamEntry = { + name, + feedId: feed.feedId, + assetClass: resolveAssetClass(feed), + schema: resolveStreamSchema(type, feed), + } + if (tradingHours && tradingHours !== "—") entry.tradingHours = tradingHours + return [entry] + }) +} + +/** Strips the " Data Feeds" suffix Chainlink appends to chain group titles. */ +function resolveChainName(chainTitle: string, chainPage: string): string { + // Handles "Arbitrum Data Feeds" → "Arbitrum" and "Data Feeds" (Ethereum) → "Ethereum" + const stripped = chainTitle.replace(/\s*Data Feeds$/, "").trim() + return stripped || chainPage.charAt(0).toUpperCase() + chainPage.slice(1) +} + +export function collectFeedEntries( + type: DataFeedType, + networkFilter: string | null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + chainCache: Record, + options: FeedMarkdownOptions = {} +): FeedEntry[] { + const visibilityOpts = { schemaFilter: options.schemaFilter } + const entries: FeedEntry[] = [] + + for (const chain of CHAINS) { + const chainData = chainCache[chain.page] + if (!chainData?.networks) continue + const chainName = resolveChainName(chain.title, chain.page) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const network of chainData.networks as any[]) { + if (!network?.metadata?.length) continue + if (networkFilter && network.queryString !== networkFilter) continue + if (options.networkType && network.networkType !== options.networkType) continue + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const visibleFeeds = network.metadata.filter((feed: any) => isFeedVisible(feed, type, "", visibilityOpts)) + if (visibleFeeds.length === 0) continue + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const feed of visibleFeeds as any[]) { + const svr = resolveSVR(feed) + const entry: FeedEntry = { + name: feed.name || feed.assetName || "Unknown", + proxyAddress: feed.proxyAddress || feed.transmissionsAccount || feed.contractAddress || "N/A", + deviation: formatDeviation(feed.threshold), + heartbeat: formatHeartbeat(feed.heartbeat), + network: network.queryString, + networkName: network.name, + networkType: network.networkType, + chain: chainName, + } + if (svr) entry.svr = svr + entries.push(entry) + } + } + } + + // Sort alphabetically by feed name within each network for consistent, scannable output + entries.sort((a, b) => { + if (a.network !== b.network) return a.network.localeCompare(b.network) + return a.name.localeCompare(b.name) + }) + + return entries +} + +// --------------------------------------------------------------------------- +// Markdown formatter +// --------------------------------------------------------------------------- + +/** + * Builds a plain-markdown document listing feed addresses or stream IDs + * for the given type, optionally filtered to a single network. + */ +export function buildFeedAddressMarkdown( + type: DataFeedType, + networkFilter: string | null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + chainCache: Record, + siteBase = "https://docs.chain.link", + options: FeedMarkdownOptions = {} +): string { + const lines: string[] = [] + const streams = isStreamsType(type) + const label = FEED_TYPE_LABELS[type] + const baseUrl = `${siteBase}/api/feeds/addresses?type=${type}` + const visibilityOpts = { schemaFilter: options.schemaFilter } + + const mainnetQueryStrings: string[] = [] + const testnetQueryStrings: string[] = [] + + if (!streams) { + for (const chain of CHAINS) { + const chainData = chainCache[chain.page] + if (!chainData?.networks) continue + // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const network of chainData.networks as any[]) { + if (!network?.queryString) continue + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const hasFeeds = network.metadata?.some((feed: any) => isFeedVisible(feed, type, "", visibilityOpts)) + if (!hasFeeds) continue + if (network.networkType === "mainnet") mainnetQueryStrings.push(network.queryString) + else testnetQueryStrings.push(network.queryString) + } + } + } + + if (streams) { + lines.push(`# Chainlink Data Streams: ${label}`) + lines.push(`Source: ${siteBase}/data-streams`) + lines.push(`Machine-readable endpoint: ${siteBase}/api/streams/ids?type=${options.publicType ?? type}`) + lines.push(`Static snapshot: ${siteBase}/data-feeds/feed-addresses/${type}.txt`) + lines.push("") + lines.push( + `> Stream IDs for Chainlink **${label}**. Stream IDs are universal — the same ID is valid across all supported networks.` + ) + lines.push(`> Supported networks and verifier proxy addresses: \`${siteBase}/api/streams/networks\``) + lines.push("") + + const streamEntries = collectStreamEntries(type, chainCache, options) + if (streamEntries.length > 0) { + lines.push("| Stream | Feed ID | Asset Class | Schema | Trading Hours |") + lines.push("|--------|---------|-------------|--------|---------------|") + for (const entry of streamEntries) { + lines.push( + `| ${entry.name} | \`${entry.feedId}\` | ${entry.assetClass} | ${entry.schema} | ${entry.tradingHours ?? "—"} |` + ) + } + lines.push("") + } else { + lines.push(`No stream IDs found for type \`${type}\`.`) + } + } else { + lines.push(`# Chainlink Feed Addresses: ${label}`) + lines.push(`Source: ${siteBase}/data-feeds/price-feeds/addresses`) + lines.push(`Machine-readable endpoint: ${baseUrl}`) + lines.push(`Static snapshot: ${siteBase}/data-feeds/feed-addresses/${type}.txt`) + lines.push("") + lines.push(`> Feed contract addresses for Chainlink **${label}** across all supported networks.`) + lines.push( + `> To narrow results to a single network, append \`&network={queryString}\` — e.g. \`${baseUrl}&network=${mainnetQueryStrings[0] ?? "ethereum-mainnet"}\`` + ) + lines.push(`> Full network list with queryStrings: \`${siteBase}/api/feeds/networks?type=${type}\``) + lines.push("") + if (mainnetQueryStrings.length > 0) { + lines.push(`**Mainnet queryStrings:** ${mainnetQueryStrings.map((q) => `\`${q}\``).join(", ")}`) + lines.push("") + } + if (testnetQueryStrings.length > 0) { + lines.push(`**Testnet queryStrings:** ${testnetQueryStrings.map((q) => `\`${q}\``).join(", ")}`) + lines.push("") + } + + const feedEntries = collectFeedEntries(type, networkFilter, chainCache, options) + if (feedEntries.length === 0) { + lines.push( + networkFilter + ? `No feeds found for type \`${type}\` on network \`${networkFilter}\`. Check the network queryString or omit the \`network\` parameter to see all networks.` + : `No feeds found for type \`${type}\`.` + ) + } else { + let currentNetwork = "" + for (const entry of feedEntries) { + if (entry.network !== currentNetwork) { + currentNetwork = entry.network + const network = feedEntries.find((e) => e.network === currentNetwork)! + lines.push(`## ${entry.chain} — ${network.networkName}`) + lines.push(`- Network type: ${network.networkType}`) + lines.push(`- Query string: \`${network.network}\``) + lines.push("") + lines.push("| Feed Name | Proxy Address | Deviation | Heartbeat |") + lines.push("|-----------|--------------|-----------|-----------|") + } + const svrLabel = entry.svr === "shared" ? " (Shared SVR)" : entry.svr === "aave" ? " (Aave SVR)" : "" + const name = escapePipes(`${entry.name}${svrLabel}`) + lines.push(`| ${name} | \`${entry.proxyAddress}\` | ${entry.deviation} | ${entry.heartbeat} |`) + } + lines.push("") + } + } + + return lines.join("\n") +} diff --git a/src/features/feeds/utils/streamMetadata.ts b/src/features/feeds/utils/streamMetadata.ts new file mode 100644 index 00000000000..f93551548fd --- /dev/null +++ b/src/features/feeds/utils/streamMetadata.ts @@ -0,0 +1,130 @@ +/** + * Utilities for resolving display metadata from Data Streams feed entries. + * + * Stream source data is inconsistent in some fields (schema, decimals, pair names). + * The functions here centralize inference logic so it stays easy to update when + * the upstream data improves. + * + * Schema mapping (see /data-streams/reference/report-schema-overview): + * crypto → v3 (Crypto Advanced) or v3-dex (DEX State Price) + * rwa → v8 (RWA Standard) or v11 (RWA Advanced) + * exchangeRate → v7 (Redemption Rates) + * smartdata → v9 (SmartData) + * tokenizedAsset → v10 (Tokenized Asset) + */ + +import type { DataFeedType } from "../components/FeedList.tsx" + +/** + * Maps stable public API type names to internal DataFeedType values. + * Update right-hand values here if internal names change. + * Do not change public keys without a deprecation period. + */ +export const STREAM_CATEGORY_MAP: Record = { + crypto: "streamsCrypto", + rwa: "streamsRwa", + smartdata: "streamsNav", + exchangeRate: "streamsExRate", + tokenizedAsset: "streamsBacked", +} + +/** + * Escapes backslashes and pipe characters for safe rendering in markdown tables. + * Backslashes are escaped first to prevent them from interfering with pipe escaping. + */ +export function escapePipes(value: string): string { + return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|") +} + +/** + * Returns the display pair name ("BTC/USD") from a stream feed. + * Returns null for incomplete entries that should be skipped. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function resolveStreamPair(feed: any): string | null { + const base = Array.isArray(feed.pair) && feed.pair.length >= 2 ? feed.pair[0] : "" + const quote = Array.isArray(feed.pair) && feed.pair.length >= 2 ? feed.pair[1] : "" + if (base && quote) return `${base}/${quote}` + const fromName = escapePipes((feed.name || "").replace(/-Streams-.*$/, "")) + return fromName || null +} + +// Normalize raw assetClass values from source data to human-readable display strings. +// Some values are internal camelCase identifiers that need to be mapped. +// TODO: remove once source data exposes clean display names. +const ASSET_CLASS_DISPLAY: Record = { + TokenizedEquities: "Tokenized Equities", +} + +/** + * Returns the asset class display string. + * Suppresses redundant subclass when it equals the main class + * (e.g. assetClass="Crypto", assetSubClass="Crypto" → "Crypto"). + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function resolveAssetClass(feed: any): string { + const rawMain = feed.docs?.assetClass || "" + const rawSub = feed.docs?.assetSubClass || "" + const main = ASSET_CLASS_DISPLAY[rawMain] ?? rawMain + const sub = ASSET_CLASS_DISPLAY[rawSub] ?? rawSub + // Compare raw values to catch cases where main and sub are the same internal identifier + if (main && sub && rawSub !== rawMain) return escapePipes(`${main} — ${sub}`) + return escapePipes(main || sub || "—") +} + +/** + * Returns the trading hours label for a stream feed. + * Derived from assetSubClass when present, falling back to clicProductName parsing. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function resolveTradingHours(feed: any): string { + const sub = feed.docs?.assetSubClass || "" + const clic = feed.docs?.clicProductName || "" + if ( + sub === "Regular Hours" || + (clic.includes("RegularHours") && !clic.includes("ExtendedHours") && !clic.includes("OvernightHours")) + ) { + return "Regular Hours (9:30am–4:00pm Mon–Fri ET)" + } + if (sub === "Extended Hours" || clic.includes("ExtendedHours")) { + return "Extended Hours (4:00am–9:30am & 4:00pm–8:00pm Mon–Fri ET)" + } + if (sub === "Overnight Hours" || clic.includes("OvernightHours")) { + return "Overnight Hours (8:00pm–4:00am Sun evening–Fri morning ET)" + } + return "—" +} + +/** + * Returns the report schema version for a stream feed. + * + * Most categories have a fixed schema. RWA is the exception — v8 (Standard) + * and v11 (Advanced) are inferred from the clicProductName suffix until the + * source data exposes a clean schema field on every feed. + * + * TODO: simplify the RWA case once feed.docs.schema is reliable upstream. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function resolveStreamSchema(type: DataFeedType, feed: any): string { + switch (type) { + case "streamsCrypto": + return feed.docs?.feedType === "Crypto-DEX" ? "v3-dex" : "v3" + case "streamsExRate": + return "v7" + case "streamsNav": + return "v9" + case "streamsBacked": + return "v10" + case "streamsRwa": { + // Infer v8 vs v11 from the numeric suffix on clicProductName (e.g. "-011" → v11) + const match = (feed.docs?.clicProductName || "").match(/-0(\d{2})$/) + if (match) { + if (match[1] === "11") return "v11" + if (match[1] === "04" || match[1] === "08") return "v8" + } + return "—" + } + default: + return "—" + } +} diff --git a/src/pages/[...path].md.ts b/src/pages/[...path].md.ts index 93be892199a..e59ba810690 100644 --- a/src/pages/[...path].md.ts +++ b/src/pages/[...path].md.ts @@ -5,26 +5,19 @@ import { textPlainHeaders } from "@lib/api/cacheHeaders.js" import { transformPageToMarkdown } from "@lib/markdown/transformMarkdown.js" import { extractFrontmatter, getIsoStringOrUndefined, toCanonicalUrl, toContentRelative } from "@lib/markdown/utils.js" +// Feeds +import { getServerSideChainMetadata } from "~/features/data/api/backend" +import { CHAINS } from "~/features/data/chains" +import { buildFeedAddressMarkdown } from "~/features/feeds/utils/feedOutput" + +// Streams (reuse same builder + data source) +import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata" + const SITE_BASE = "https://docs.chain.link" const CONTENT_ROOT = path.resolve("src/content") const LLMS_DIRECTIVE = "> For the complete documentation index, see [llms.txt](/llms.txt)." -const MARKDOWN_REDIRECTS: Record = { - "ccip/tutorials/cross-chain-tokens": "ccip/tutorials/evm/cross-chain-tokens", - - // Data Streams - "data-streams/getting-started": "data-streams/tutorials/streams-trade/getting-started", - "data-streams/getting-started-hardhat": "data-streams/tutorials/streams-trade/getting-started-hardhat", - "data-streams/reference/streams-direct/streams-direct-onchain-verification": - "data-streams/reference/onchain-verification", - - // Newly surfaced redirects - "chainlink-functions/resources/concepts": "chainlink-functions/resources", - "cre/getting-started/conclusion": "cre/getting-started", - "data-streams/reference/streams-direct/streams-direct-interface-ws": "data-streams/reference/interface-ws", -} - const markdownHeaders = { ...textPlainHeaders, "Content-Type": "text/markdown; charset=utf-8", @@ -39,93 +32,7 @@ export const GET: APIRoute = async ({ params, request }) => { return new Response("Page not found.", { status: 404 }) } - const specialResolution = await resolveSpecialCanonicalMarkdownPath(cleanPath) - if (specialResolution) { - return buildMarkdownResponseFromPath(specialResolution.resolvedPath, request, specialResolution.sourceCanonicalPath) - } - - const creResolution = await resolveCreCanonicalMarkdownPath(cleanPath) - - if (creResolution.kind === "selector") { - return new Response(buildCreSelectorMarkdown(cleanPath, creResolution), { - status: 200, - headers: markdownHeaders, - }) - } - - const resolvedPath = creResolution.kind === "resolved" ? creResolution.path : cleanPath - return buildMarkdownResponseFromPath(resolvedPath, request) -} - -type SpecialResolution = { - resolvedPath: string - sourceCanonicalPath: string -} - -async function resolveSpecialCanonicalMarkdownPath(cleanPath: string): Promise { - const specialPathMap: Record = { - "cre-templates": "cre/templates", - } - - const mappedPath = specialPathMap[cleanPath] - if (!mappedPath) return null - - const file = await findContentFile(mappedPath) - if (!file) return null - - return { - resolvedPath: mappedPath, - sourceCanonicalPath: cleanPath, - } -} - -type CreResolution = - | { kind: "none" } - | { kind: "resolved"; path: string } - | { kind: "selector"; goPath: string; tsPath: string } - -async function resolveCreCanonicalMarkdownPath(cleanPath: string): Promise { - if (!cleanPath.startsWith("cre/")) { - return { kind: "none" } - } - - const direct = await findContentFile(cleanPath) - if (direct) { - return { kind: "resolved", path: cleanPath } - } - - const goPath = `${cleanPath}-go` - const tsPath = `${cleanPath}-ts` - - const [goFile, tsFile] = await Promise.all([findContentFile(goPath), findContentFile(tsPath)]) - - if (goFile && tsFile) { - return { kind: "selector", goPath, tsPath } - } - - if (goFile) { - return { kind: "resolved", path: goPath } - } - - if (tsFile) { - return { kind: "resolved", path: tsPath } - } - - return { kind: "none" } -} - -async function buildMarkdownResponseFromPath( - resolvedPath: string, - request: Request, - sourceCanonicalPathOverride?: string -): Promise { - const redirectTarget = MARKDOWN_REDIRECTS[resolvedPath] - - if (redirectTarget) { - return buildMarkdownMovedResponse(resolvedPath, redirectTarget) - } - - const mdxAbsPath = await findContentFile(resolvedPath) + const mdxAbsPath = await findContentFile(cleanPath) if (!mdxAbsPath) { return new Response("Page not found.", { status: 404 }) @@ -137,20 +44,20 @@ async function buildMarkdownResponseFromPath( const raw = await fs.readFile(mdxAbsPath, "utf-8") const { body, fmTitle, fmLastModified } = extractFrontmatter(raw) - const section = resolvedPath.split("/")[0] - - const transformed = await transformPageBodyToMarkdown(body, mdxAbsPath, { + const transformed = await transformPageBodyToMarkdown(body, mdxAbsPath, cleanPath, { siteBase: SITE_BASE, targetLanguage, }) const relFromContent = toContentRelative(mdxAbsPath) - const derivedSourceUrl = toCanonicalUrl(section, relFromContent, SITE_BASE) - const sourceUrl = sourceCanonicalPathOverride ? `${SITE_BASE}/${sourceCanonicalPathOverride}` : derivedSourceUrl + const sourceUrl = toCanonicalUrl(cleanPath.split("/")[0], relFromContent, SITE_BASE) const title = fmTitle || path.basename(mdxAbsPath, path.extname(mdxAbsPath)) const lastModified = getIsoStringOrUndefined(fmLastModified) + const isFeedsPage = cleanPath.includes("data-feeds/price-feeds/addresses") + const isStreamsPage = cleanPath.includes("data-streams") + const headerLines = [ `# ${title}`, `Source: ${sourceUrl}`, @@ -158,6 +65,33 @@ async function buildMarkdownResponseFromPath( "", LLMS_DIRECTIVE, "", + ...(isFeedsPage + ? [ + "## Full datasets", + "", + "Use the network index to retrieve feed addresses:", + "", + "/data-feeds/feed-addresses/default.txt", + "", + "Each network has its own dataset.", + "Do not load multiple networks unless required.", + "", + ] + : []), + ...(isStreamsPage + ? [ + "## Full datasets", + "", + "Use structured datasets for stream IDs and network metadata:", + "", + "/data-streams/stream-ids/crypto.txt", + "/data-streams/networks.txt", + "", + "Stream IDs are universal.", + "Networks provide verifier proxy addresses.", + "", + ] + : []), ] return new Response([...headerLines, transformed.trim()].join("\n"), { @@ -169,21 +103,75 @@ async function buildMarkdownResponseFromPath( async function transformPageBodyToMarkdown( body: string, mdxAbsPath: string, + routePath: string, options: { siteBase: string targetLanguage?: string } ): Promise { - // Targeted fix for problematic page - if (mdxAbsPath.includes("data-feeds/deprecating-feeds")) { - return ` -## Deprecated Feeds + const chainCache = await getServerSideChainMetadata(CHAINS) + + // ----------------------- + // FEEDS INJECTION + // ----------------------- + if (body.includes("/g, replacement) + } + + // ----------------------- + // STREAMS INJECTION + // ----------------------- + if (body.includes("/g, replacement) } try { @@ -199,6 +187,24 @@ https://docs.chain.link/data-feeds/deprecating-feeds } } +// ----------------------- +// STREAM EXAMPLE BUILDER +// ----------------------- +function buildStreamExample(streams: any[]): string { + const sample = streams.slice(0, 10) + + const lines = ["| Stream | Feed ID | Schema |", "|--------|---------|--------|"] + + for (const s of sample) { + lines.push(`| ${s.name} | \`${s.feedId}\` | ${s.schema} |`) + } + + return lines.join("\n") +} + +// ----------------------- +// UTILITIES +// ----------------------- function buildFallbackMarkdownBody(body: string): string { return stripRuntimeMdxSyntax(body) .replace(/<([A-Z][A-Za-z0-9]*)\b[^>]*\/>/g, "") @@ -247,9 +253,7 @@ function stripRuntimeMdxSyntax(body: string): string { continue } - if (/^export\s+(const|let|var)\s+/.test(trimmed)) { - continue - } + if (/^export\s+(const|let|var)\s+/.test(trimmed)) continue output.push(line) } @@ -294,37 +298,3 @@ async function findContentFile(cleanPath: string): Promise { return null } - -function buildMarkdownMovedResponse(sourcePath: string, targetPath: string): Response { - const sourceUrl = `${SITE_BASE}/${sourcePath}` - const targetUrl = `/${targetPath}.md` - - return new Response( - [ - `# Redirect`, - `Source: ${sourceUrl}`, - "", - LLMS_DIRECTIVE, - "", - "This page has moved.", - "", - `Use the current documentation: [${targetPath}](${targetUrl}).`, - "", - ].join("\n"), - { status: 200, headers: markdownHeaders } - ) -} - -function buildCreSelectorMarkdown(canonicalPath: string, resolution: any): string { - const canonicalUrl = `${SITE_BASE}/${canonicalPath}` - return [ - `# ${canonicalPath}`, - `Source: ${canonicalUrl}`, - "", - LLMS_DIRECTIVE, - "", - `- Go: /${resolution.goPath}.md`, - `- TypeScript: /${resolution.tsPath}.md`, - "", - ].join("\n") -} diff --git a/src/pages/data-feeds/feed-addresses/[type].txt.ts b/src/pages/data-feeds/feed-addresses/[type].txt.ts new file mode 100644 index 00000000000..3f05b9ba03a --- /dev/null +++ b/src/pages/data-feeds/feed-addresses/[type].txt.ts @@ -0,0 +1,95 @@ +import type { APIRoute } from "astro" +import { getServerSideChainMetadata } from "~/features/data/api/backend.ts" +import { CHAINS } from "~/features/data/chains.ts" +import { VALID_FEED_TYPES } from "~/features/feeds/utils/feedOutput.ts" +import type { DataFeedType } from "~/features/feeds/components/FeedList.tsx" + +export function getStaticPaths() { + return VALID_FEED_TYPES.map((type) => ({ + params: { type }, + })) +} + +export const GET: APIRoute = async ({ params }) => { + const type = params.type as DataFeedType + + if (!VALID_FEED_TYPES.includes(type)) { + return new Response(`Invalid type "${type}"`, { status: 400 }) + } + + const chainCache = await getServerSideChainMetadata(CHAINS) + + const networks: { + queryString: string + networkName: string + chain: string + networkType: string + }[] = [] + + const seen = new Set() + + for (const chain of Object.values(chainCache)) { + for (const network of chain.networks ?? []) { + const queryString = network.queryString + if (!queryString) continue + + if (seen.has(queryString)) continue + seen.add(queryString) + + networks.push({ + queryString, + networkName: network.name, + chain: network.chain || "", + networkType: network.networkType || "mainnet", + }) + } + } + + // Sort: mainnet first, then testnet + networks.sort((a, b) => { + if (a.networkType !== b.networkType) { + return a.networkType === "mainnet" ? -1 : 1 + } + return a.queryString.localeCompare(b.queryString) + }) + + const lines: string[] = [] + + lines.push(`# Chainlink Feed Addresses Index (${type})`) + lines.push("") + lines.push("This document lists all available networks for this feed type.") + lines.push("") + lines.push("Each network has a dedicated dataset.") + lines.push("") + lines.push("Do not load multiple networks unless required.") + lines.push("Each file contains the complete dataset for one network.") + lines.push("") + + lines.push("## Networks") + lines.push("") + + for (const net of networks) { + lines.push(`- ${net.queryString} → /data-feeds/feed-addresses/${type}/${net.queryString}.txt`) + lines.push(` name: ${net.networkName}`) + lines.push(` type: ${net.networkType}`) + if (net.chain) { + lines.push(` chain: ${net.chain}`) + } + lines.push("") + } + + lines.push("## Usage pattern") + lines.push("") + lines.push("1. Select a network from the list above") + lines.push("2. Fetch the corresponding dataset") + lines.push("3. Filter by feed name as needed") + lines.push("") + + return new Response(lines.join("\n"), { + status: 200, + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800", + }, + }) +} diff --git a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts new file mode 100644 index 00000000000..974770a561d --- /dev/null +++ b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts @@ -0,0 +1,74 @@ +import type { APIRoute } from "astro" +import { getServerSideChainMetadata } from "~/features/data/api/backend.ts" +import { CHAINS } from "~/features/data/chains.ts" +import { buildFeedAddressMarkdown, VALID_FEED_TYPES } from "~/features/feeds/utils/feedOutput.ts" +import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata.ts" +import type { DataFeedType } from "~/features/feeds/components/FeedList.tsx" + +// Reverse map: internal → public (streamsCrypto → crypto) +const INTERNAL_TO_PUBLIC: Record = Object.fromEntries( + Object.entries(STREAM_CATEGORY_MAP).map(([pub, internal]) => [internal, pub]) +) + +export async function getStaticPaths() { + const chainCache = await getServerSideChainMetadata(CHAINS) + + const paths: { params: { type: string; network: string } }[] = [] + const seen = new Set() + + for (const type of VALID_FEED_TYPES) { + for (const chain of Object.values(chainCache)) { + for (const network of chain.networks ?? []) { + const queryString = network.queryString + + // Guardrails + if (!queryString) continue + + const key = `${type}:${queryString}` + if (seen.has(key)) continue + seen.add(key) + + paths.push({ + params: { + type, + network: queryString, + }, + }) + } + } + } + + return paths +} + +export const GET: APIRoute = async ({ params }) => { + const type = params.type as DataFeedType + const network = params.network + + if (!VALID_FEED_TYPES.includes(type)) { + return new Response(`Invalid type "${type}"`, { status: 400 }) + } + + const publicType = INTERNAL_TO_PUBLIC[type] + + const chainCache = await getServerSideChainMetadata(CHAINS) + + const markdown = buildFeedAddressMarkdown( + type, + network, // ✅ scoped per-network + chainCache, + "https://docs.chain.link", + { + publicType, + } + ) + + return new Response(markdown, { + status: 200, + headers: { + "Content-Type": "text/plain; charset=utf-8", + // CDN-friendly caching + "Cache-Control": "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800", + }, + }) +} diff --git a/src/pages/data-streams/networks.txt.ts b/src/pages/data-streams/networks.txt.ts new file mode 100644 index 00000000000..f889453d4e7 --- /dev/null +++ b/src/pages/data-streams/networks.txt.ts @@ -0,0 +1,105 @@ +import type { APIRoute } from "astro" +import { StreamsNetworksData } from "~/features/feeds/data/StreamsNetworksData.ts" +import { textPlainHeaders } from "@lib/api/cacheHeaders.js" + +export const prerender = false + +interface StreamNetworkEntry { + network: string + label: string + verifierProxy: string +} + +function getNetworks(): { + mainnet: StreamNetworkEntry[] + testnet: StreamNetworkEntry[] +} { + const mainnet: StreamNetworkEntry[] = [] + const testnet: StreamNetworkEntry[] = [] + + for (const entry of StreamsNetworksData) { + if (entry.isCanton) continue + + if (entry.mainnet?.verifierProxy) { + mainnet.push({ + network: entry.network, + label: entry.mainnet.label, + verifierProxy: entry.mainnet.verifierProxy, + }) + } + + if (entry.testnet?.verifierProxy) { + testnet.push({ + network: entry.network, + label: entry.testnet.label, + verifierProxy: entry.testnet.verifierProxy, + }) + } + } + + return { mainnet, testnet } +} + +function buildMarkdown(): string { + const { mainnet, testnet } = getNetworks() + + const lines: string[] = [ + "# Chainlink Data Streams Networks", + "", + "Supported networks and verifier proxy addresses.", + "", + "Verifier proxies are shared across all Data Streams categories and schema versions.", + "", + "Use the verifier proxy for the network where your application is deployed.", + "", + "---", + "", + "## Mainnet Networks", + "", + "| Network | Label | Verifier Proxy |", + "|---------|-------|----------------|", + ] + + for (const n of mainnet) { + lines.push(`| ${n.network} | ${n.label} | \`${n.verifierProxy}\` |`) + } + + if (testnet.length > 0) { + lines.push( + "", + "## Testnet Networks", + "", + "| Network | Label | Verifier Proxy |", + "|---------|-------|----------------|" + ) + + for (const n of testnet) { + lines.push(`| ${n.network} | ${n.label} | \`${n.verifierProxy}\` |`) + } + } + + lines.push( + "", + "---", + "", + "Use this file together with stream ID datasets:", + "", + "- /data-streams/stream-ids/{type}.txt", + "", + "Stream IDs are universal. Networks provide verifier proxy addresses." + ) + + return lines.join("\n") +} + +export const GET: APIRoute = async () => { + const markdown = buildMarkdown() + + return new Response(markdown, { + status: 200, + headers: { + ...textPlainHeaders, + "Cache-Control": "public, max-age=3600, s-maxage=86400", + }, + }) +} diff --git a/src/pages/data-streams/stream-ids/[type].txt.ts b/src/pages/data-streams/stream-ids/[type].txt.ts new file mode 100644 index 00000000000..74ca1f62614 --- /dev/null +++ b/src/pages/data-streams/stream-ids/[type].txt.ts @@ -0,0 +1,113 @@ +import type { APIRoute } from "astro" +import { getServerSideChainMetadata } from "~/features/data/api/backend.ts" +import { CHAINS } from "~/features/data/chains.ts" +import { buildFeedAddressMarkdown, type FeedMarkdownOptions } from "~/features/feeds/utils/feedOutput.ts" +import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata.ts" +import { textPlainHeaders } from "@lib/api/cacheHeaders.js" + +export const prerender = false + +export function getStaticPaths() { + return Object.keys(STREAM_CATEGORY_MAP).map((type) => ({ + params: { type }, + })) +} + +export const GET: APIRoute = async ({ params }) => { + const rawType = params.type as string + + const internalType = STREAM_CATEGORY_MAP[rawType] + if (!internalType) { + const valid = Object.keys(STREAM_CATEGORY_MAP).join(", ") + return new Response(`Invalid type "${rawType}". Valid values: ${valid}`, { + status: 400, + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }) + } + + const chainCache = await getServerSideChainMetadata(CHAINS) + + const options: FeedMarkdownOptions = { + publicType: rawType, + } + + let markdown = buildFeedAddressMarkdown(internalType, null, chainCache, "https://docs.chain.link", options) + + // -------------------------------------------------- + // CLEANUP: remove API + cross-product references + // -------------------------------------------------- + + markdown = markdown + .replace(/Machine-readable endpoint:[^\n]*\n?/g, "") + .replace(/Supported networks and verifier proxy addresses:[^\n]*\n?/g, "") + .replace(/Static snapshot:[^\n]*\n?/g, "") + + // -------------------------------------------------- + // SCHEMA DETECTION + // -------------------------------------------------- + + const schemaMatches = [...markdown.matchAll(/\|\s*[^|]+\s*\|\s*`?0x[a-fA-F0-9]+`?\s*\|\s*[^|]*\|\s*(v\d+)/g)] + + const schemas = Array.from(new Set(schemaMatches.map((m) => m[1]))) + + // -------------------------------------------------- + // SCHEMA → DOCS MAP + // -------------------------------------------------- + + const SCHEMA_DOCS: Record = { + v3: "https://docs.chain.link/data-streams/reference/report-schema-v3", + v7: "https://docs.chain.link/data-streams/reference/report-schema-v7", + v8: "https://docs.chain.link/data-streams/reference/report-schema-v8", + v9: "https://docs.chain.link/data-streams/reference/report-schema-v9", + v10: "https://docs.chain.link/data-streams/reference/report-schema-v10", + v11: "https://docs.chain.link/data-streams/reference/report-schema-v11", + } + + // -------------------------------------------------- + // BUILD INTRO BLOCK (single structured section) + // -------------------------------------------------- + + const introLines = [ + `> Stream IDs for Chainlink Data Streams – ${capitalize(rawType)}.`, + `> These IDs are universal and valid across all supported networks.`, + `> To use a stream ID, retrieve the verifier proxy for the target network from /data-streams/networks.txt.`, + `> Datasets may contain multiple schema versions. Filter by schema if needed.`, + ] + + if (schemas.length > 0) { + introLines.push(`> Schemas present in this dataset:`) + + for (const s of schemas) { + const url = SCHEMA_DOCS[s] + if (url) { + introLines.push(`> - \`${s}\` → ${url}`) + } else { + introLines.push(`> - ${s}`) + } + } + } + + const introBlock = introLines.join("\n") + "\n" + + // -------------------------------------------------- + // REPLACE ORIGINAL INTRO + // -------------------------------------------------- + + markdown = markdown.replace(/> Stream IDs[\s\S]*?\n/, introBlock) + + return new Response(markdown.trim(), { + status: 200, + headers: { + ...textPlainHeaders, + "Cache-Control": "public, max-age=3600, s-maxage=86400", + }, + }) +} + +// ----------------------- +// Utility +// ----------------------- + +function capitalize(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1) +} From 4c67c3d8266f907ea6f1b0a858e3a6807af8c4b2 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 13:37:59 -0700 Subject: [PATCH 02/28] typcheck fix --- src/pages/[...path].md.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/[...path].md.ts b/src/pages/[...path].md.ts index e59ba810690..c248c8e6b36 100644 --- a/src/pages/[...path].md.ts +++ b/src/pages/[...path].md.ts @@ -147,9 +147,7 @@ ${exampleMarkdown} // STREAMS INJECTION // ----------------------- if (body.includes(" Date: Wed, 13 May 2026 13:46:24 -0700 Subject: [PATCH 03/28] fix --- src/features/feeds/utils/feedOutput.ts | 141 ++++++------------------- 1 file changed, 34 insertions(+), 107 deletions(-) diff --git a/src/features/feeds/utils/feedOutput.ts b/src/features/feeds/utils/feedOutput.ts index 4c8c91f0ff0..070f97fc6f5 100644 --- a/src/features/feeds/utils/feedOutput.ts +++ b/src/features/feeds/utils/feedOutput.ts @@ -58,18 +58,11 @@ export function formatDeviation(threshold: number): string { } export interface FeedMarkdownOptions { - /** Filter by schema version, e.g. "v8" or "v11". Only meaningful for streamsRwa. */ schemaFilter?: string - /** Public-facing API type name for use in generated URLs (e.g. "crypto" instead of "streamsCrypto"). */ publicType?: string - /** Restrict to "mainnet" or "testnet" networks only. */ networkType?: "mainnet" | "testnet" } -// --------------------------------------------------------------------------- -// Structured data types — used for JSON output -// --------------------------------------------------------------------------- - export interface StreamEntry { name: string feedId: string @@ -86,15 +79,10 @@ export interface FeedEntry { network: string networkName: string networkType: string - /** Blockchain ecosystem name (e.g. "Ethereum", "Arbitrum"). */ chain: string svr?: "shared" | "aave" } -// --------------------------------------------------------------------------- -// Shared data collectors — used by both markdown and JSON formatters -// --------------------------------------------------------------------------- - // eslint-disable-next-line @typescript-eslint/no-explicit-any function resolveSVR(feed: any): "shared" | "aave" | undefined { const isShared = typeof feed.path === "string" && /-shared-svr$/.test(feed.path) @@ -109,7 +97,10 @@ export function collectStreamEntries( chainCache: Record, options: FeedMarkdownOptions = {} ): StreamEntry[] { - const visibilityOpts = { schemaFilter: options.schemaFilter } + const visibilityOpts = { + ...(options.schemaFilter ? { schemaFilter: options.schemaFilter } : {}), + } as any + const seenFeedIds = new Set() // eslint-disable-next-line @typescript-eslint/no-explicit-any const raw: any[] = [] @@ -117,14 +108,15 @@ export function collectStreamEntries( for (const chain of CHAINS) { const chainData = chainCache[chain.page] if (!chainData?.networks) continue - // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const network of chainData.networks as any[]) { if (!network?.metadata?.length) continue if (network.networkType !== "mainnet") continue - // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const feed of network.metadata as any[]) { if (!isFeedVisible(feed, type, "", visibilityOpts)) continue if (!feed.feedId || seenFeedIds.has(feed.feedId)) continue + seenFeedIds.add(feed.feedId) raw.push(feed) } @@ -133,25 +125,28 @@ export function collectStreamEntries( raw.sort((a, b) => (resolveStreamPair(a) ?? "").localeCompare(resolveStreamPair(b) ?? "")) - // eslint-disable-next-line @typescript-eslint/no-explicit-any return raw.flatMap((feed: any) => { const name = resolveStreamPair(feed) if (!name) return [] + const tradingHours = resolveTradingHours(feed) + const entry: StreamEntry = { name, feedId: feed.feedId, assetClass: resolveAssetClass(feed), schema: resolveStreamSchema(type, feed), } - if (tradingHours && tradingHours !== "—") entry.tradingHours = tradingHours + + if (tradingHours && tradingHours !== "—") { + entry.tradingHours = tradingHours + } + return [entry] }) } -/** Strips the " Data Feeds" suffix Chainlink appends to chain group titles. */ function resolveChainName(chainTitle: string, chainPage: string): string { - // Handles "Arbitrum Data Feeds" → "Arbitrum" and "Data Feeds" (Ethereum) → "Ethereum" const stripped = chainTitle.replace(/\s*Data Feeds$/, "").trim() return stripped || chainPage.charAt(0).toUpperCase() + chainPage.slice(1) } @@ -159,29 +154,33 @@ function resolveChainName(chainTitle: string, chainPage: string): string { export function collectFeedEntries( type: DataFeedType, networkFilter: string | null, - // eslint-disable-next-line @typescript-eslint/no-explicit-any chainCache: Record, options: FeedMarkdownOptions = {} ): FeedEntry[] { - const visibilityOpts = { schemaFilter: options.schemaFilter } + const visibilityOpts = { + ...(options.schemaFilter ? { schemaFilter: options.schemaFilter } : {}), + } as any + const entries: FeedEntry[] = [] for (const chain of CHAINS) { const chainData = chainCache[chain.page] if (!chainData?.networks) continue + const chainName = resolveChainName(chain.title, chain.page) - // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const network of chainData.networks as any[]) { if (!network?.metadata?.length) continue if (networkFilter && network.queryString !== networkFilter) continue if (options.networkType && network.networkType !== options.networkType) continue - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const visibleFeeds = network.metadata.filter((feed: any) => isFeedVisible(feed, type, "", visibilityOpts)) + if (visibleFeeds.length === 0) continue - // eslint-disable-next-line @typescript-eslint/no-explicit-any for (const feed of visibleFeeds as any[]) { const svr = resolveSVR(feed) + const entry: FeedEntry = { name: feed.name || feed.assetName || "Unknown", proxyAddress: feed.proxyAddress || feed.transmissionsAccount || feed.contractAddress || "N/A", @@ -192,13 +191,14 @@ export function collectFeedEntries( networkType: network.networkType, chain: chainName, } + if (svr) entry.svr = svr + entries.push(entry) } } } - // Sort alphabetically by feed name within each network for consistent, scannable output entries.sort((a, b) => { if (a.network !== b.network) return a.network.localeCompare(b.network) return a.name.localeCompare(b.name) @@ -207,119 +207,46 @@ export function collectFeedEntries( return entries } -// --------------------------------------------------------------------------- -// Markdown formatter -// --------------------------------------------------------------------------- - -/** - * Builds a plain-markdown document listing feed addresses or stream IDs - * for the given type, optionally filtered to a single network. - */ export function buildFeedAddressMarkdown( type: DataFeedType, networkFilter: string | null, - // eslint-disable-next-line @typescript-eslint/no-explicit-any chainCache: Record, siteBase = "https://docs.chain.link", options: FeedMarkdownOptions = {} ): string { const lines: string[] = [] const streams = isStreamsType(type) + const label = FEED_TYPE_LABELS[type] - const baseUrl = `${siteBase}/api/feeds/addresses?type=${type}` - const visibilityOpts = { schemaFilter: options.schemaFilter } - - const mainnetQueryStrings: string[] = [] - const testnetQueryStrings: string[] = [] - - if (!streams) { - for (const chain of CHAINS) { - const chainData = chainCache[chain.page] - if (!chainData?.networks) continue - // eslint-disable-next-line @typescript-eslint/no-explicit-any - for (const network of chainData.networks as any[]) { - if (!network?.queryString) continue - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const hasFeeds = network.metadata?.some((feed: any) => isFeedVisible(feed, type, "", visibilityOpts)) - if (!hasFeeds) continue - if (network.networkType === "mainnet") mainnetQueryStrings.push(network.queryString) - else testnetQueryStrings.push(network.queryString) - } - } - } if (streams) { lines.push(`# Chainlink Data Streams: ${label}`) lines.push(`Source: ${siteBase}/data-streams`) - lines.push(`Machine-readable endpoint: ${siteBase}/api/streams/ids?type=${options.publicType ?? type}`) - lines.push(`Static snapshot: ${siteBase}/data-feeds/feed-addresses/${type}.txt`) lines.push("") + lines.push(`> Stream IDs for Chainlink Data Streams – ${label.replace("Data Streams — ", "")}.`) + lines.push(`> These IDs are universal and valid across all supported networks.`) lines.push( - `> Stream IDs for Chainlink **${label}**. Stream IDs are universal — the same ID is valid across all supported networks.` + `> To use a stream ID, retrieve the verifier proxy for the target network from /data-streams/networks.txt.` ) - lines.push(`> Supported networks and verifier proxy addresses: \`${siteBase}/api/streams/networks\``) + lines.push(`> Datasets may contain multiple schema versions. Filter by schema if needed.`) lines.push("") const streamEntries = collectStreamEntries(type, chainCache, options) + if (streamEntries.length > 0) { lines.push("| Stream | Feed ID | Asset Class | Schema | Trading Hours |") lines.push("|--------|---------|-------------|--------|---------------|") + for (const entry of streamEntries) { lines.push( - `| ${entry.name} | \`${entry.feedId}\` | ${entry.assetClass} | ${entry.schema} | ${entry.tradingHours ?? "—"} |` + `| ${entry.name} | \`${entry.feedId}\` | ${entry.assetClass} | \`${entry.schema}\` | ${entry.tradingHours ?? "—"} |` ) } + lines.push("") } else { lines.push(`No stream IDs found for type \`${type}\`.`) } - } else { - lines.push(`# Chainlink Feed Addresses: ${label}`) - lines.push(`Source: ${siteBase}/data-feeds/price-feeds/addresses`) - lines.push(`Machine-readable endpoint: ${baseUrl}`) - lines.push(`Static snapshot: ${siteBase}/data-feeds/feed-addresses/${type}.txt`) - lines.push("") - lines.push(`> Feed contract addresses for Chainlink **${label}** across all supported networks.`) - lines.push( - `> To narrow results to a single network, append \`&network={queryString}\` — e.g. \`${baseUrl}&network=${mainnetQueryStrings[0] ?? "ethereum-mainnet"}\`` - ) - lines.push(`> Full network list with queryStrings: \`${siteBase}/api/feeds/networks?type=${type}\``) - lines.push("") - if (mainnetQueryStrings.length > 0) { - lines.push(`**Mainnet queryStrings:** ${mainnetQueryStrings.map((q) => `\`${q}\``).join(", ")}`) - lines.push("") - } - if (testnetQueryStrings.length > 0) { - lines.push(`**Testnet queryStrings:** ${testnetQueryStrings.map((q) => `\`${q}\``).join(", ")}`) - lines.push("") - } - - const feedEntries = collectFeedEntries(type, networkFilter, chainCache, options) - if (feedEntries.length === 0) { - lines.push( - networkFilter - ? `No feeds found for type \`${type}\` on network \`${networkFilter}\`. Check the network queryString or omit the \`network\` parameter to see all networks.` - : `No feeds found for type \`${type}\`.` - ) - } else { - let currentNetwork = "" - for (const entry of feedEntries) { - if (entry.network !== currentNetwork) { - currentNetwork = entry.network - const network = feedEntries.find((e) => e.network === currentNetwork)! - lines.push(`## ${entry.chain} — ${network.networkName}`) - lines.push(`- Network type: ${network.networkType}`) - lines.push(`- Query string: \`${network.network}\``) - lines.push("") - lines.push("| Feed Name | Proxy Address | Deviation | Heartbeat |") - lines.push("|-----------|--------------|-----------|-----------|") - } - const svrLabel = entry.svr === "shared" ? " (Shared SVR)" : entry.svr === "aave" ? " (Aave SVR)" : "" - const name = escapePipes(`${entry.name}${svrLabel}`) - lines.push(`| ${name} | \`${entry.proxyAddress}\` | ${entry.deviation} | ${entry.heartbeat} |`) - } - lines.push("") - } } return lines.join("\n") From 174608ab591a43a4c87156c5a0048ba1a3ce1c38 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 13:57:00 -0700 Subject: [PATCH 04/28] typecheck --- src/pages/[...path].md.ts | 16 ++++++------- .../data-feeds/feed-addresses/[type].txt.ts | 7 ++++-- .../feed-addresses/[type]/[network].txt.ts | 23 ++++++------------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/pages/[...path].md.ts b/src/pages/[...path].md.ts index c248c8e6b36..6aad606724b 100644 --- a/src/pages/[...path].md.ts +++ b/src/pages/[...path].md.ts @@ -1,18 +1,16 @@ import type { APIRoute } from "astro" import fs from "node:fs/promises" import path from "node:path" + +import { getServerSideChainMetadata } from "~/features/data/api/backend.ts" +import { CHAINS } from "~/features/data/chains.ts" +import { buildFeedAddressMarkdown, collectStreamEntries } from "~/features/feeds/utils/feedOutput.ts" +import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata.ts" + import { textPlainHeaders } from "@lib/api/cacheHeaders.js" import { transformPageToMarkdown } from "@lib/markdown/transformMarkdown.js" import { extractFrontmatter, getIsoStringOrUndefined, toCanonicalUrl, toContentRelative } from "@lib/markdown/utils.js" -// Feeds -import { getServerSideChainMetadata } from "~/features/data/api/backend" -import { CHAINS } from "~/features/data/chains" -import { buildFeedAddressMarkdown } from "~/features/feeds/utils/feedOutput" - -// Streams (reuse same builder + data source) -import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata" - const SITE_BASE = "https://docs.chain.link" const CONTENT_ROOT = path.resolve("src/content") @@ -188,7 +186,7 @@ ${exampleMarkdown} // ----------------------- // STREAM EXAMPLE BUILDER // ----------------------- -function buildStreamExample(streams: any[]): string { +function buildStreamExample(streams: Array<{ name: string; feedId: string; schema: string }>): string { const sample = streams.slice(0, 10) const lines = ["| Stream | Feed ID | Schema |", "|--------|---------|--------|"] diff --git a/src/pages/data-feeds/feed-addresses/[type].txt.ts b/src/pages/data-feeds/feed-addresses/[type].txt.ts index 3f05b9ba03a..af3d09ab2dd 100644 --- a/src/pages/data-feeds/feed-addresses/[type].txt.ts +++ b/src/pages/data-feeds/feed-addresses/[type].txt.ts @@ -29,7 +29,10 @@ export const GET: APIRoute = async ({ params }) => { const seen = new Set() for (const chain of Object.values(chainCache)) { - for (const network of chain.networks ?? []) { + // ✅ FIX: rename to avoid shadowing + const chainNetworks = (chain as { networks?: any[] }).networks ?? [] + + for (const network of chainNetworks) { const queryString = network.queryString if (!queryString) continue @@ -39,7 +42,7 @@ export const GET: APIRoute = async ({ params }) => { networks.push({ queryString, networkName: network.name, - chain: network.chain || "", + chain: typeof network.chain === "string" ? network.chain : "", networkType: network.networkType || "mainnet", }) } diff --git a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts index 974770a561d..2c0a6414f82 100644 --- a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts +++ b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts @@ -5,7 +5,7 @@ import { buildFeedAddressMarkdown, VALID_FEED_TYPES } from "~/features/feeds/uti import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata.ts" import type { DataFeedType } from "~/features/feeds/components/FeedList.tsx" -// Reverse map: internal → public (streamsCrypto → crypto) +// Reverse map: internal → public const INTERNAL_TO_PUBLIC: Record = Object.fromEntries( Object.entries(STREAM_CATEGORY_MAP).map(([pub, internal]) => [internal, pub]) ) @@ -18,10 +18,10 @@ export async function getStaticPaths() { for (const type of VALID_FEED_TYPES) { for (const chain of Object.values(chainCache)) { - for (const network of chain.networks ?? []) { - const queryString = network.queryString + const chainNetworks = (chain as { networks?: any[] }).networks ?? [] - // Guardrails + for (const network of chainNetworks) { + const queryString = network.queryString if (!queryString) continue const key = `${type}:${queryString}` @@ -43,31 +43,22 @@ export async function getStaticPaths() { export const GET: APIRoute = async ({ params }) => { const type = params.type as DataFeedType - const network = params.network + const network = params.network ?? null // ✅ FIX if (!VALID_FEED_TYPES.includes(type)) { return new Response(`Invalid type "${type}"`, { status: 400 }) } - const publicType = INTERNAL_TO_PUBLIC[type] + const publicType = INTERNAL_TO_PUBLIC[type] ?? type // ✅ SAFE const chainCache = await getServerSideChainMetadata(CHAINS) - const markdown = buildFeedAddressMarkdown( - type, - network, // ✅ scoped per-network - chainCache, - "https://docs.chain.link", - { - publicType, - } - ) + const markdown = buildFeedAddressMarkdown(type, network, chainCache, "https://docs.chain.link", { publicType }) return new Response(markdown, { status: 200, headers: { "Content-Type": "text/plain; charset=utf-8", - // CDN-friendly caching "Cache-Control": "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800", }, }) From ace719c1b5cb1a0bd501da2274c8028e331935ab Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 14:24:08 -0700 Subject: [PATCH 05/28] final tweaks --- src/pages/[...path].md.ts | 62 +++++++++++++++------------------------ 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/src/pages/[...path].md.ts b/src/pages/[...path].md.ts index 6aad606724b..477ca1b23f3 100644 --- a/src/pages/[...path].md.ts +++ b/src/pages/[...path].md.ts @@ -53,9 +53,6 @@ export const GET: APIRoute = async ({ params, request }) => { const title = fmTitle || path.basename(mdxAbsPath, path.extname(mdxAbsPath)) const lastModified = getIsoStringOrUndefined(fmLastModified) - const isFeedsPage = cleanPath.includes("data-feeds/price-feeds/addresses") - const isStreamsPage = cleanPath.includes("data-streams") - const headerLines = [ `# ${title}`, `Source: ${sourceUrl}`, @@ -63,33 +60,6 @@ export const GET: APIRoute = async ({ params, request }) => { "", LLMS_DIRECTIVE, "", - ...(isFeedsPage - ? [ - "## Full datasets", - "", - "Use the network index to retrieve feed addresses:", - "", - "/data-feeds/feed-addresses/default.txt", - "", - "Each network has its own dataset.", - "Do not load multiple networks unless required.", - "", - ] - : []), - ...(isStreamsPage - ? [ - "## Full datasets", - "", - "Use structured datasets for stream IDs and network metadata:", - "", - "/data-streams/stream-ids/crypto.txt", - "/data-streams/networks.txt", - "", - "Stream IDs are universal.", - "Networks provide verifier proxy addresses.", - "", - ] - : []), ] return new Response([...headerLines, transformed.trim()].join("\n"), { @@ -107,7 +77,13 @@ async function transformPageBodyToMarkdown( targetLanguage?: string } ): Promise { - const chainCache = await getServerSideChainMetadata(CHAINS) + let chainCache: Record = {} + + try { + chainCache = await getServerSideChainMetadata(CHAINS) + } catch (e) { + console.error("Failed to load chain metadata:", e) + } // ----------------------- // FEEDS INJECTION @@ -121,6 +97,14 @@ async function transformPageBodyToMarkdown( { networkType: "mainnet" } ) + const hasExample = exampleMarkdown && !exampleMarkdown.includes("No feeds found") + + const fallbackExample = ` +| Feed Name | Proxy Address | Deviation | Heartbeat | +|-----------|--------------|-----------|-----------| +| ETH / USD | \`0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419\` | 0.5% | 1h | +` + const replacement = ` ## Full datasets @@ -135,7 +119,7 @@ Do not load multiple networks unless required. ## Example (Ethereum Mainnet) -${exampleMarkdown} +${hasExample ? exampleMarkdown : fallbackExample} ` body = body.replace(//g, replacement) @@ -147,7 +131,13 @@ ${exampleMarkdown} if (body.includes(" 0 ? buildStreamExample(streams) : "" + + const fallbackExample = ` +| Stream | Feed ID | Schema | +|--------|---------|--------| +| BTC/USD | \`0x00039d9f...\` | v3 | +` const replacement = ` ## Full datasets @@ -164,7 +154,7 @@ Networks provide verifier proxy addresses. ## Example (Crypto Streams) -${exampleMarkdown} +${exampleMarkdown || fallbackExample} ` body = body.replace(//g, replacement) @@ -183,8 +173,6 @@ ${exampleMarkdown} } } -// ----------------------- -// STREAM EXAMPLE BUILDER // ----------------------- function buildStreamExample(streams: Array<{ name: string; feedId: string; schema: string }>): string { const sample = streams.slice(0, 10) @@ -198,8 +186,6 @@ function buildStreamExample(streams: Array<{ name: string; feedId: string; schem return lines.join("\n") } -// ----------------------- -// UTILITIES // ----------------------- function buildFallbackMarkdownBody(body: string): string { return stripRuntimeMdxSyntax(body) From 63b562e44bcd6cf5eaf328127a2b16a24d9f40fc Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 14:41:51 -0700 Subject: [PATCH 06/28] llms enhancement --- src/content/data-feeds/llms.txt | 25 +++++++++++++++++-------- src/content/data-streams/llms.txt | 25 +++++++++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/content/data-feeds/llms.txt b/src/content/data-feeds/llms.txt index 189c85d9471..5bb0b163c18 100644 --- a/src/content/data-feeds/llms.txt +++ b/src/content/data-feeds/llms.txt @@ -42,15 +42,24 @@ Use address datasets only when retrieving current feed addresses. Use structured datasets when retrieving feed addresses. Documentation pages may contain partial or example data only. -Start with the dataset index: +Select the dataset that matches the feed category: -- Standard price feeds: https://docs.chain.link/data-feeds/feed-addresses/default.txt -- Rates feeds: https://docs.chain.link/data-feeds/feed-addresses/rates.txt -- SmartData feeds: https://docs.chain.link/data-feeds/feed-addresses/smartdata.txt -- U.S. Government Macroeconomic Data Feeds: https://docs.chain.link/data-feeds/feed-addresses/usGovernmentMacroeconomicData.txt -- Tokenized equity feeds: https://docs.chain.link/data-feeds/feed-addresses/tokenizedEquity.txt +- Standard price feeds (e.g. ETH/USD, BTC/USD): + https://docs.chain.link/data-feeds/feed-addresses/default.txt -Each index lists available networks. Each network is identified by its `queryString`. +- Rates feeds: + https://docs.chain.link/data-feeds/feed-addresses/rates.txt + +- SmartData feeds (Proof of Reserve, NAVLink, SmartAUM): + https://docs.chain.link/data-feeds/feed-addresses/smartdata.txt + +- U.S. Government Macroeconomic Data Feeds: + https://docs.chain.link/data-feeds/feed-addresses/usGovernmentMacroeconomicData.txt + +- Tokenized equity feeds: + https://docs.chain.link/data-feeds/feed-addresses/tokenizedEquity.txt + +Each dataset lists available networks. Each network is identified by its `queryString`. Retrieve a specific dataset: @@ -58,7 +67,7 @@ Retrieve a specific dataset: Usage pattern: -1. Open the index file for the desired feed type +1. Select the dataset that matches the feed category 2. Identify the target network `queryString` 3. Retrieve the corresponding dataset 4. Filter by feed name as needed diff --git a/src/content/data-streams/llms.txt b/src/content/data-streams/llms.txt index b89432197b9..43cab00c06c 100644 --- a/src/content/data-streams/llms.txt +++ b/src/content/data-streams/llms.txt @@ -46,13 +46,26 @@ Additional Data and Lifecycle Use structured datasets to retrieve stream IDs and network metadata. -Start with the dataset for the desired category: +First determine the correct stream category and schema: -- Crypto streams: https://docs.chain.link/data-streams/stream-ids/crypto.txt -- RWA streams: https://docs.chain.link/data-streams/stream-ids/rwa.txt -- SmartData streams: https://docs.chain.link/data-streams/stream-ids/smartdata.txt -- Exchange rate streams: https://docs.chain.link/data-streams/stream-ids/exchangeRate.txt -- Tokenized asset streams: https://docs.chain.link/data-streams/stream-ids/tokenizedAsset.txt +- https://docs.chain.link/data-streams/reference/report-schema-overview.md + +Then select the dataset that matches that category: + +- Cryptocurrency streams (e.g. BTC/USD, ETH/USD): + https://docs.chain.link/data-streams/stream-ids/crypto.txt + +- Real World Asset (RWA) streams: + https://docs.chain.link/data-streams/stream-ids/rwa.txt + +- SmartData streams: + https://docs.chain.link/data-streams/stream-ids/smartdata.txt + +- Exchange rate streams: + https://docs.chain.link/data-streams/stream-ids/exchangeRate.txt + +- Tokenized asset streams: + https://docs.chain.link/data-streams/stream-ids/tokenizedAsset.txt Stream IDs are universal and valid across all supported networks. From 2dd7d6c5c0cc8b2328082a48db00b0549ed0ea4b Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 15:11:07 -0700 Subject: [PATCH 07/28] fix network.tct file generation --- src/features/feeds/utils/feedOutput.ts | 40 ++++++++++++++++-- .../feed-addresses/[type]/[network].txt.ts | 42 ++++--------------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/features/feeds/utils/feedOutput.ts b/src/features/feeds/utils/feedOutput.ts index 070f97fc6f5..d672a935291 100644 --- a/src/features/feeds/utils/feedOutput.ts +++ b/src/features/feeds/utils/feedOutput.ts @@ -93,7 +93,6 @@ function resolveSVR(feed: any): "shared" | "aave" | undefined { export function collectStreamEntries( type: DataFeedType, - // eslint-disable-next-line @typescript-eslint/no-explicit-any chainCache: Record, options: FeedMarkdownOptions = {} ): StreamEntry[] { @@ -102,7 +101,6 @@ export function collectStreamEntries( } as any const seenFeedIds = new Set() - // eslint-disable-next-line @typescript-eslint/no-explicit-any const raw: any[] = [] for (const chain of CHAINS) { @@ -216,7 +214,6 @@ export function buildFeedAddressMarkdown( ): string { const lines: string[] = [] const streams = isStreamsType(type) - const label = FEED_TYPE_LABELS[type] if (streams) { @@ -247,6 +244,43 @@ export function buildFeedAddressMarkdown( } else { lines.push(`No stream IDs found for type \`${type}\`.`) } + } else { + lines.push(`# Chainlink Feed Addresses: ${label}`) + lines.push("") + + const feedEntries = collectFeedEntries(type, networkFilter, chainCache, options) + + if (feedEntries.length === 0) { + lines.push( + networkFilter + ? `No feeds found for type \`${type}\` on network \`${networkFilter}\`.` + : `No feeds found for type \`${type}\`.` + ) + } else { + let currentNetwork = "" + + for (const entry of feedEntries) { + if (entry.network !== currentNetwork) { + currentNetwork = entry.network + const network = feedEntries.find((e) => e.network === currentNetwork) + + if (!network) continue + + lines.push(`## ${entry.chain} — ${network.networkName}`) + lines.push(`- Network type: ${network.networkType}`) + lines.push(`- Query string: \`${network.network}\``) + lines.push("") + lines.push("| Feed Name | Proxy Address | Deviation | Heartbeat |") + lines.push("|-----------|--------------|-----------|-----------|") + } + + const name = escapePipes(entry.name) + + lines.push(`| ${name} | \`${entry.proxyAddress}\` | ${entry.deviation} | ${entry.heartbeat} |`) + } + + lines.push("") + } } return lines.join("\n") diff --git a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts index 2c0a6414f82..9c4c2a6bb5d 100644 --- a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts +++ b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts @@ -5,54 +5,28 @@ import { buildFeedAddressMarkdown, VALID_FEED_TYPES } from "~/features/feeds/uti import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata.ts" import type { DataFeedType } from "~/features/feeds/components/FeedList.tsx" -// Reverse map: internal → public +export const prerender = false + const INTERNAL_TO_PUBLIC: Record = Object.fromEntries( Object.entries(STREAM_CATEGORY_MAP).map(([pub, internal]) => [internal, pub]) ) -export async function getStaticPaths() { - const chainCache = await getServerSideChainMetadata(CHAINS) - - const paths: { params: { type: string; network: string } }[] = [] - const seen = new Set() - - for (const type of VALID_FEED_TYPES) { - for (const chain of Object.values(chainCache)) { - const chainNetworks = (chain as { networks?: any[] }).networks ?? [] - - for (const network of chainNetworks) { - const queryString = network.queryString - if (!queryString) continue - - const key = `${type}:${queryString}` - if (seen.has(key)) continue - seen.add(key) - - paths.push({ - params: { - type, - network: queryString, - }, - }) - } - } - } - - return paths -} - export const GET: APIRoute = async ({ params }) => { const type = params.type as DataFeedType - const network = params.network ?? null // ✅ FIX + const network = params.network ?? null if (!VALID_FEED_TYPES.includes(type)) { return new Response(`Invalid type "${type}"`, { status: 400 }) } - const publicType = INTERNAL_TO_PUBLIC[type] ?? type // ✅ SAFE + const publicType = INTERNAL_TO_PUBLIC[type] ?? type const chainCache = await getServerSideChainMetadata(CHAINS) + if (!chainCache || Object.keys(chainCache).length === 0) { + return new Response("Failed to load feed data", { status: 500 }) + } + const markdown = buildFeedAddressMarkdown(type, network, chainCache, "https://docs.chain.link", { publicType }) return new Response(markdown, { From 1aab1a832f93da0372bcbb2d9226a407a3dcd88d Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 15:21:22 -0700 Subject: [PATCH 08/28] guard + debug --- .../feed-addresses/[type]/[network].txt.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts index 9c4c2a6bb5d..4f2f9449754 100644 --- a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts +++ b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts @@ -13,7 +13,7 @@ const INTERNAL_TO_PUBLIC: Record = Object.fromEntries( export const GET: APIRoute = async ({ params }) => { const type = params.type as DataFeedType - const network = params.network ?? null + const network = typeof params.network === "string" ? params.network : null if (!VALID_FEED_TYPES.includes(type)) { return new Response(`Invalid type "${type}"`, { status: 400 }) @@ -21,14 +21,44 @@ export const GET: APIRoute = async ({ params }) => { const publicType = INTERNAL_TO_PUBLIC[type] ?? type - const chainCache = await getServerSideChainMetadata(CHAINS) + let chainCache: Record = {} - if (!chainCache || Object.keys(chainCache).length === 0) { - return new Response("Failed to load feed data", { status: 500 }) + try { + chainCache = await getServerSideChainMetadata(CHAINS) + } catch (e) { + console.error("Failed to fetch chain metadata:", e) + } + + // ✅ Validate actual usable data (not just object presence) + const hasNetworks = Object.values(chainCache).some( + (chain: any) => Array.isArray(chain?.networks) && chain.networks.length > 0 + ) + + if (!hasNetworks) { + console.error("Chain cache missing networks or empty:", chainCache) + + return new Response("# Feed data unavailable\n\nPreview environment could not load network metadata.", { + status: 200, + headers: { + "Content-Type": "text/plain; charset=utf-8", + }, + }) } const markdown = buildFeedAddressMarkdown(type, network, chainCache, "https://docs.chain.link", { publicType }) + // ✅ Guard against empty output (should not happen now, but safe) + if (!markdown || markdown.trim().length === 0) { + console.error("Empty markdown output:", { type, network }) + + return new Response("# No data returned\n\nThe dataset could not be generated.", { + status: 200, + headers: { + "Content-Type": "text/plain; charset=utf-8", + }, + }) + } + return new Response(markdown, { status: 200, headers: { From ca41d698d7bb62b968f3e985419a5a63a1ff3a51 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 15:52:01 -0700 Subject: [PATCH 09/28] force node --- src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts index 4f2f9449754..aba429ae68c 100644 --- a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts +++ b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts @@ -5,6 +5,7 @@ import { buildFeedAddressMarkdown, VALID_FEED_TYPES } from "~/features/feeds/uti import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata.ts" import type { DataFeedType } from "~/features/feeds/components/FeedList.tsx" +export const runtime = "nodejs" export const prerender = false const INTERNAL_TO_PUBLIC: Record = Object.fromEntries( From affd9898a8da31218d71619af2ba9f16d0a99811 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 16:04:05 -0700 Subject: [PATCH 10/28] add caching layer --- .../feed-addresses/[type]/[network].txt.ts | 85 ++++++++----------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts index aba429ae68c..bb7f3a58d0c 100644 --- a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts +++ b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts @@ -1,70 +1,59 @@ import type { APIRoute } from "astro" import { getServerSideChainMetadata } from "~/features/data/api/backend.ts" import { CHAINS } from "~/features/data/chains.ts" -import { buildFeedAddressMarkdown, VALID_FEED_TYPES } from "~/features/feeds/utils/feedOutput.ts" -import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata.ts" -import type { DataFeedType } from "~/features/feeds/components/FeedList.tsx" -export const runtime = "nodejs" export const prerender = false +export const runtime = "nodejs" -const INTERNAL_TO_PUBLIC: Record = Object.fromEntries( - Object.entries(STREAM_CATEGORY_MAP).map(([pub, internal]) => [internal, pub]) -) - -export const GET: APIRoute = async ({ params }) => { - const type = params.type as DataFeedType - const network = typeof params.network === "string" ? params.network : null - - if (!VALID_FEED_TYPES.includes(type)) { - return new Response(`Invalid type "${type}"`, { status: 400 }) - } - - const publicType = INTERNAL_TO_PUBLIC[type] ?? type - +export const GET: APIRoute = async () => { let chainCache: Record = {} try { chainCache = await getServerSideChainMetadata(CHAINS) } catch (e) { - console.error("Failed to fetch chain metadata:", e) + console.error("FAILED METADATA FETCH:", e) + + return new Response( + JSON.stringify( + { + error: "fetch_failed", + message: String(e), + }, + null, + 2 + ), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ) } - // ✅ Validate actual usable data (not just object presence) + const keys = Object.keys(chainCache) + + const sampleKey = keys[0] + const sample = sampleKey ? chainCache[sampleKey] : null + const hasNetworks = Object.values(chainCache).some( (chain: any) => Array.isArray(chain?.networks) && chain.networks.length > 0 ) - if (!hasNetworks) { - console.error("Chain cache missing networks or empty:", chainCache) - - return new Response("# Feed data unavailable\n\nPreview environment could not load network metadata.", { - status: 200, - headers: { - "Content-Type": "text/plain; charset=utf-8", + return new Response( + JSON.stringify( + { + keys, + sampleKey, + sample, + hasNetworks, }, - }) - } - - const markdown = buildFeedAddressMarkdown(type, network, chainCache, "https://docs.chain.link", { publicType }) - - // ✅ Guard against empty output (should not happen now, but safe) - if (!markdown || markdown.trim().length === 0) { - console.error("Empty markdown output:", { type, network }) - - return new Response("# No data returned\n\nThe dataset could not be generated.", { + null, + 2 + ), + { status: 200, headers: { - "Content-Type": "text/plain; charset=utf-8", + "Content-Type": "application/json", }, - }) - } - - return new Response(markdown, { - status: 200, - headers: { - "Content-Type": "text/plain; charset=utf-8", - "Cache-Control": "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800", - }, - }) + } + ) } From 91e927e3292268358d4cedbfb37f872972d06df7 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 16:15:33 -0700 Subject: [PATCH 11/28] build-time generation --- .../feed-addresses/[type]/[network].txt.ts | 121 ++++++++++-------- 1 file changed, 70 insertions(+), 51 deletions(-) diff --git a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts index bb7f3a58d0c..d000e440473 100644 --- a/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts +++ b/src/pages/data-feeds/feed-addresses/[type]/[network].txt.ts @@ -1,59 +1,78 @@ +/** + * Static snapshots of feed addresses per network, pre-rendered at build time. + * Served at /data-feeds/feed-addresses/{type}/{network}.txt + * + * For always-live data use the dynamic endpoint: /api/feeds/addresses + */ + import type { APIRoute } from "astro" import { getServerSideChainMetadata } from "~/features/data/api/backend.ts" import { CHAINS } from "~/features/data/chains.ts" +import { buildFeedAddressMarkdown, VALID_FEED_TYPES } from "~/features/feeds/utils/feedOutput.ts" +import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata.ts" +import type { DataFeedType } from "~/features/feeds/components/FeedList.tsx" + +// Reverse map: internal → public +const INTERNAL_TO_PUBLIC: Record = Object.fromEntries( + Object.entries(STREAM_CATEGORY_MAP).map(([pub, internal]) => [internal, pub]) +) + +// ✅ Build-time route generation +export async function getStaticPaths() { + const chainCache = await getServerSideChainMetadata(CHAINS) + + const paths: { params: { type: string; network: string } }[] = [] + const seen = new Set() + + for (const type of VALID_FEED_TYPES) { + for (const chain of Object.values(chainCache)) { + const networks = (chain as any).networks ?? [] -export const prerender = false -export const runtime = "nodejs" - -export const GET: APIRoute = async () => { - let chainCache: Record = {} - - try { - chainCache = await getServerSideChainMetadata(CHAINS) - } catch (e) { - console.error("FAILED METADATA FETCH:", e) - - return new Response( - JSON.stringify( - { - error: "fetch_failed", - message: String(e), - }, - null, - 2 - ), - { - status: 200, - headers: { "Content-Type": "application/json" }, + for (const network of networks) { + const queryString = network.queryString + if (!queryString) continue + + const key = `${type}:${queryString}` + if (seen.has(key)) continue + seen.add(key) + + paths.push({ + params: { + type, + network: queryString, + }, + }) } - ) + } } - const keys = Object.keys(chainCache) - - const sampleKey = keys[0] - const sample = sampleKey ? chainCache[sampleKey] : null - - const hasNetworks = Object.values(chainCache).some( - (chain: any) => Array.isArray(chain?.networks) && chain.networks.length > 0 - ) - - return new Response( - JSON.stringify( - { - keys, - sampleKey, - sample, - hasNetworks, - }, - null, - 2 - ), - { - status: 200, - headers: { - "Content-Type": "application/json", - }, - } - ) + return paths +} + +// ❌ IMPORTANT: no `prerender = false` here +// this must run at build time + +export const GET: APIRoute = async ({ params }) => { + const type = params.type as DataFeedType + const network = typeof params.network === "string" ? params.network : null + + if (!VALID_FEED_TYPES.includes(type)) { + return new Response(`Invalid type "${type}"`, { status: 400 }) + } + + const publicType = INTERNAL_TO_PUBLIC[type] + + // ✅ Safe at build time (filesystem allowed) + const chainCache = await getServerSideChainMetadata(CHAINS) + + const markdown = buildFeedAddressMarkdown(type, network, chainCache, "https://docs.chain.link", { publicType }) + + return new Response(markdown, { + status: 200, + headers: { + "Content-Type": "text/plain; charset=utf-8", + // Long CDN cache — content only changes on redeploy + "Cache-Control": "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800", + }, + }) } From 1f360120c8ce3b6cccefbda4a58936d0a9539034 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 16:25:21 -0700 Subject: [PATCH 12/28] build-time streams --- src/pages/data-streams/stream-ids/[type].txt.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/data-streams/stream-ids/[type].txt.ts b/src/pages/data-streams/stream-ids/[type].txt.ts index 74ca1f62614..12f5688eff8 100644 --- a/src/pages/data-streams/stream-ids/[type].txt.ts +++ b/src/pages/data-streams/stream-ids/[type].txt.ts @@ -5,8 +5,7 @@ import { buildFeedAddressMarkdown, type FeedMarkdownOptions } from "~/features/f import { STREAM_CATEGORY_MAP } from "~/features/feeds/utils/streamMetadata.ts" import { textPlainHeaders } from "@lib/api/cacheHeaders.js" -export const prerender = false - +// ✅ Build-time route generation export function getStaticPaths() { return Object.keys(STREAM_CATEGORY_MAP).map((type) => ({ params: { type }, @@ -25,6 +24,7 @@ export const GET: APIRoute = async ({ params }) => { }) } + // ✅ Safe at build time const chainCache = await getServerSideChainMetadata(CHAINS) const options: FeedMarkdownOptions = { @@ -64,7 +64,7 @@ export const GET: APIRoute = async ({ params }) => { } // -------------------------------------------------- - // BUILD INTRO BLOCK (single structured section) + // BUILD INTRO BLOCK // -------------------------------------------------- const introLines = [ From a7ac7e874a75ff7a5170ce7ca7f4c01683e4850d Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 16:41:38 -0700 Subject: [PATCH 13/28] add txt files to md endopint --- src/pages/[...path].md.ts | 82 ++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/src/pages/[...path].md.ts b/src/pages/[...path].md.ts index 477ca1b23f3..3287e090326 100644 --- a/src/pages/[...path].md.ts +++ b/src/pages/[...path].md.ts @@ -105,15 +105,47 @@ async function transformPageBodyToMarkdown( | ETH / USD | \`0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419\` | 0.5% | 1h | ` + // ----------------------- + // FEED TYPE DETECTION + // ----------------------- + let feedType = "default" + + if (routePath.includes("price-feeds")) { + feedType = "default" + } else if (routePath.includes("smartdata")) { + feedType = "smartdata" + } else if (routePath.includes("rates")) { + feedType = "rates" + } else if (routePath.includes("tokenized-equity")) { + feedType = "tokenizedEquity" + } else if (routePath.includes("us-government")) { + feedType = "usGovernmentMacroeconomicData" + } + + const labelMap: Record = { + default: "Price feeds", + smartdata: "SmartData feeds", + rates: "Rates feeds", + tokenizedEquity: "Tokenized equity feeds", + usGovernmentMacroeconomicData: "U.S. Government Macroeconomic Data feeds", + } + + const feedLabel = labelMap[feedType] || feedType + const replacement = ` -## Full datasets +## Feed Contract Addresses + +The interactive address table on this page is loaded dynamically and is not included in this markdown export. -Use the network index to retrieve feed addresses: +For complete and up-to-date feed addresses, use structured datasets: -/data-feeds/feed-addresses/default.txt +- ${feedLabel}${feedType === "default" ? " (default dataset)" : ""}: +/data-feeds/feed-addresses/${feedType}.txt -Each network has its own dataset. -Do not load multiple networks unless required. +- Per-network datasets: +/data-feeds/feed-addresses/${feedType}/{network}.txt + +Each dataset contains the full set of feeds for the selected network. Filter by feed name as needed. --- @@ -129,7 +161,17 @@ ${hasExample ? exampleMarkdown : fallbackExample} // STREAMS INJECTION // ----------------------- if (body.includes(" 0 ? buildStreamExample(streams) : "" @@ -139,20 +181,34 @@ ${hasExample ? exampleMarkdown : fallbackExample} | BTC/USD | \`0x00039d9f...\` | v3 | ` + const streamLabelMap: Record = { + crypto: "Crypto streams", + rwa: "RWA streams", + exchangeRate: "Exchange rate streams", + smartdata: "SmartData streams", + tokenizedAsset: "Tokenized asset streams", + } + + const streamLabel = streamLabelMap[rawType] || rawType + const replacement = ` -## Full datasets +## Stream IDs + +The interactive stream table on this page is loaded dynamically and is not included in this markdown export. + +For complete and up-to-date stream IDs, use structured datasets: -Use structured datasets: +- ${streamLabel}: +/data-streams/stream-ids/${rawType}.txt -- /data-streams/stream-ids/crypto.txt -- /data-streams/networks.txt +- Supported networks: +/data-streams/networks.txt -Stream IDs are universal. -Networks provide verifier proxy addresses. +Stream IDs are universal. Use the verifier proxy for your target network when consuming them. --- -## Example (Crypto Streams) +## Example (${streamLabel}) ${exampleMarkdown || fallbackExample} ` From f9231e37938c40d5702a5d6829d8f81116febc96 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 16:55:19 -0700 Subject: [PATCH 14/28] update llms to point to next txt files for addresses --- src/content/data-feeds/llms.txt | 11 +++++++++-- src/content/data-streams/llms.txt | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/content/data-feeds/llms.txt b/src/content/data-feeds/llms.txt index 5bb0b163c18..6589ea337cc 100644 --- a/src/content/data-feeds/llms.txt +++ b/src/content/data-feeds/llms.txt @@ -4,7 +4,9 @@ Prefer specific `.md` pages. Do not rely on memorized feed addresses. -Use address datasets only when retrieving current feed addresses. +You must use address datasets when retrieving feed addresses. + +Do not use documentation pages or llms-full.txt to retrieve feed addresses. ## Overview and Setup @@ -40,7 +42,10 @@ Use address datasets only when retrieving current feed addresses. ## Feed Address Retrieval -Use structured datasets when retrieving feed addresses. Documentation pages may contain partial or example data only. +You must use structured datasets to retrieve feed addresses. + +Documentation pages and llms-full.txt do not contain complete feed address data. +Do not attempt to extract feed addresses from documentation pages or dynamic tables. Select the dataset that matches the feed category: @@ -65,6 +70,8 @@ Retrieve a specific dataset: - https://docs.chain.link/data-feeds/feed-addresses/{type}/{network}.txt +For a specific network, you should directly retrieve the per-network dataset instead of scanning the full index. + Usage pattern: 1. Select the dataset that matches the feed category diff --git a/src/content/data-streams/llms.txt b/src/content/data-streams/llms.txt index 43cab00c06c..f334a82ccb8 100644 --- a/src/content/data-streams/llms.txt +++ b/src/content/data-streams/llms.txt @@ -4,7 +4,9 @@ Prefer specific `.md` pages. Choose the stream category and report schema before implementation. -Do not rely on memorized supported networks, stream IDs, verifier addresses, or schema details. +You must use structured datasets to retrieve stream IDs and network metadata. + +Do not use documentation pages or llms-full.txt to retrieve stream IDs, schemas, or verifier addresses. ## Overview and Concepts @@ -22,6 +24,7 @@ These pages may include data-driven or dynamically rendered content. Do not infe - [Stream Categories and Report Schemas](https://docs.chain.link/data-streams/reference/report-schema-overview.md): Start here to choose the correct category and schema. Primary Categories + - [Cryptocurrency Streams](https://docs.chain.link/data-streams/crypto-streams) - [Exchange Rate Streams](https://docs.chain.link/data-streams/exchange-rate-streams) - [Real World Asset (RWA) Streams](https://docs.chain.link/data-streams/rwa-streams) @@ -44,7 +47,10 @@ Additional Data and Lifecycle ## Stream ID Retrieval -Use structured datasets to retrieve stream IDs and network metadata. +You must use structured datasets to retrieve stream IDs and network metadata. + +Documentation pages and llms-full.txt do not contain complete stream ID or network data. +Do not attempt to extract stream IDs or verifier addresses from documentation pages or dynamic tables. First determine the correct stream category and schema: @@ -73,13 +79,15 @@ To retrieve verifier proxy addresses: - https://docs.chain.link/data-streams/networks.txt +You must use the networks dataset to determine the correct verifier proxy for a given network. + ## Retrieval Pattern -1. Select the appropriate stream category +1. Select the correct stream category and schema 2. Retrieve the dataset for that category 3. Identify the required stream ID 4. Retrieve network metadata from `/data-streams/networks.txt` -5. You must use the verifier proxy for the network where your application is deployed +5. Use the verifier proxy for the target network ## Constraints From 3e1b0039b531ada5961b2a32c89b8847f86473aa Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 17:10:10 -0700 Subject: [PATCH 15/28] update global llms priorities --- public/llms.txt | 140 +++++++++++++++++++----------------------------- 1 file changed, 56 insertions(+), 84 deletions(-) diff --git a/public/llms.txt b/public/llms.txt index baab91f1831..9a6025d2eab 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -12,13 +12,50 @@ For multi-step tasks, retrieve and combine multiple specific pages rather than r Use product documentation indexes for deeper product-specific navigation. -Use `llms-full.txt` only when broad product-level context is required. +Use `llms-full.txt` only for conceptual understanding or documentation context. -Defaults: -- Prefer CRE for new multi-step, offchain, or automation workflows unless another product is explicitly required -- Prefer VRF v2.5 for randomness -- Treat Any API and `/chainlink-nodes/v1/` as legacy +Do not use `llms-full.txt` to retrieve structured data such as: +- feed addresses +- stream IDs +- network metadata +Structured data must be retrieved from dataset endpoints. + +--- + +## Live Data and Structured Datasets + +You must use structured datasets when retrieving current network data, addresses, or routing information. + +Do not scrape documentation pages or interactive tables for structured data. + +When retrieving structured data, always prefer dataset endpoints over documentation pages. + +### Data Feeds + +- Index (select network): + https://docs.chain.link/data-feeds/feed-addresses/default.txt + +- Per-network datasets: + https://docs.chain.link/data-feeds/feed-addresses/{type}/{network}.txt + +### Data Streams + +- Stream ID datasets: + https://docs.chain.link/data-streams/stream-ids/crypto.txt + +- Network metadata (verifier proxies): + https://docs.chain.link/data-streams/networks.txt + +### CCIP Directory + +- CCIP Mainnet Directory: + https://docs.chain.link/ccip/directory/mainnet + +- CCIP Testnet Directory: + https://docs.chain.link/ccip/directory/testnet + +--- ## Core Documentation @@ -27,6 +64,7 @@ Defaults: - [LINK Token Contracts](https://docs.chain.link/resources/link-token-contracts.md): Use when funding contracts or configuring Chainlink services. Do not rely on memorized values. - [Fund Your Smart Contract](https://docs.chain.link/resources/fund-your-contract.md): Fund contracts with LINK before interacting with Chainlink services. +--- ## Chainlink Runtime Environment (CRE) @@ -52,6 +90,7 @@ Reference - [CLI Reference](https://docs.chain.link/cre/reference/cli.md) - [SDK Overview (TS)](https://docs.chain.link/cre/reference/sdk/overview-ts.md) +--- ## CRE Connect @@ -65,6 +104,7 @@ Private beta — permissioned access. Use these pages to understand the product - [Smart Accounts](https://docs.chain.link/crec/concepts/smart-accounts.md) - [Extensions](https://docs.chain.link/crec/extensions.md) +--- ## CCIP @@ -93,6 +133,7 @@ Reference - [Aptos Move module interfaces reference](https://docs.chain.link/ccip/api-reference/aptos.md) - [TON contract interfaces reference](https://docs.chain.link/ccip/api-reference/ton.md) +--- ## Data Feeds @@ -100,7 +141,7 @@ Overview and Setup - [Overview](https://docs.chain.link/data-feeds.md) - [Getting Started](https://docs.chain.link/data-feeds/getting-started.md) - [Selecting Data Feeds](https://docs.chain.link/data-feeds/selecting-data-feeds.md) -- [Feed Types](https://docs.chain.link/data-feeds/feed-types.md): Choose the correct feed category before retrieving addresses or implementation details. +- [Feed Types](https://docs.chain.link/data-feeds/feed-types.md) Feed Categories - [Price Feeds](https://docs.chain.link/data-feeds/price-feeds.md) @@ -119,62 +160,17 @@ Advanced and Reference - [API Reference](https://docs.chain.link/data-feeds/api-reference.md) - [Historical Data](https://docs.chain.link/data-feeds/historical-data.md) +--- ## Data Streams - [Overview](https://docs.chain.link/data-streams.md) - [Architecture](https://docs.chain.link/data-streams/architecture.md) -- [Stream Categories and Report Schemas](https://docs.chain.link/data-streams/reference/report-schema-overview.md): Choose the correct stream category and schema before implementation. +- [Stream Categories and Report Schemas](https://docs.chain.link/data-streams/reference/report-schema-overview.md) - [Supported Networks](https://docs.chain.link/data-streams/supported-networks.md) - [API Reference](https://docs.chain.link/data-streams/reference/data-streams-api.md) - -## DataLink - -- [Overview](https://docs.chain.link/datalink.md) -- [Pull Delivery Overview](https://docs.chain.link/datalink/pull-delivery/overview.md) -- [Architecture](https://docs.chain.link/datalink/pull-delivery/architecture.md) - - -## Automated Compliance Engine (ACE) - -- [Overview](https://docs.chain.link/ace.md) -- [Getting Started](https://docs.chain.link/ace/getting-started.md) -- [Concepts](https://docs.chain.link/ace/concepts/architecture.md) -- [API Reference](https://docs.chain.link/ace/reference/api/coordinator.md) - - -## DTA Technical Standard - -- [Overview](https://docs.chain.link/dta-technical-standard.md) -- [How It Works](https://docs.chain.link/dta-technical-standard/how-it-works.md) -- [Architecture](https://docs.chain.link/dta-technical-standard/concepts/architecture.md) -- [Actors](https://docs.chain.link/dta-technical-standard/actors.md) - - -## Chainlink Automation - -- [Overview](https://docs.chain.link/chainlink-automation.md) -- [Getting Started](https://docs.chain.link/chainlink-automation/overview/getting-started.md) -- [Automation Architecture](https://docs.chain.link/chainlink-automation/concepts/automation-architecture.md) -- [Service Limits](https://docs.chain.link/chainlink-automation/overview/service-limits.md) - - -## Chainlink Functions - -- [Overview](https://docs.chain.link/chainlink-functions.md) -- [Getting Started](https://docs.chain.link/chainlink-functions/getting-started.md) -- [Architecture](https://docs.chain.link/chainlink-functions/resources/architecture.md) -- [API Reference](https://docs.chain.link/chainlink-functions/api-reference/functions-client.md) - - -## VRF - -- [Overview](https://docs.chain.link/vrf.md) -- [Getting Started v2.5](https://docs.chain.link/vrf/v2-5/getting-started.md) -- [Best Practices](https://docs.chain.link/vrf/v2-5/best-practices.md) -- [Migration Guides](https://docs.chain.link/vrf/v2-5/migration-from-v2.md) -- [Supported Networks](https://docs.chain.link/vrf/v2-5/supported-networks.md) +--- ## Coverage model @@ -184,6 +180,8 @@ For full coverage: - Use product documentation indexes for structured navigation - Use full documentation bundles when broad context is required +--- + ## Product Documentation Indexes Use these for deeper product-level navigation. These indexes provide broader coverage than the curated sections above while preserving page-level retrieval. @@ -199,39 +197,13 @@ Use these for deeper product-level navigation. These indexes provide broader cov - [DTA Technical Standard Documentation Index](https://docs.chain.link/dta-technical-standard/llms.txt) - [VRF Documentation Index](https://docs.chain.link/vrf/llms.txt) -## Live Data and Structured Datasets - -Use structured datasets when retrieving current network data, addresses, or routing information. - -Do not scrape documentation pages or interactive tables for structured data. - -### Data Feeds - -- Index (select network): - https://docs.chain.link/data-feeds/feed-addresses/default.txt - -- Per-network datasets: - https://docs.chain.link/data-feeds/feed-addresses/{type}/{network}.txt - -### Data Streams - -- Stream ID datasets: - https://docs.chain.link/data-streams/stream-ids/crypto.txt - -- Network metadata (verifier proxies): - https://docs.chain.link/data-streams/networks.txt - -### CCIP Directory - -- CCIP Mainnet Directory: - https://docs.chain.link/ccip/directory/mainnet - -- CCIP Testnet Directory: - https://docs.chain.link/ccip/directory/testnet +--- ## Full Documentation Bundles -Use `llms-full.txt` only when broad product-level context is required or when the relevant page cannot be identified from the curated index. +Use only for conceptual understanding or when no dataset or specific page can be identified. + +Do not use `llms-full.txt` for retrieving structured data such as feed addresses, stream IDs, or network metadata. - [CRE Full](https://docs.chain.link/cre/llms-full-ts.txt) - [CRE Connect Full](https://docs.chain.link/crec/llms-full.txt) From 55fd9654ce4d92c6d96c6b6f3cdb3ea522071a24 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 13 May 2026 18:03:51 -0700 Subject: [PATCH 16/28] add txt directive to html --- src/content/data-feeds/llms-full.txt | 42 ++++- .../data-feeds/price-feeds/addresses.mdx | 11 ++ .../data-feeds/rates-feeds/addresses.mdx | 11 ++ .../data-feeds/smartdata/addresses.mdx | 11 ++ .../tokenized-equity-feeds/index.mdx | 11 ++ .../us-government-macroeconomic/addresses.mdx | 13 ++ .../data-streams/crypto-streams/index.mdx | 17 ++ .../exchange-rate-streams/index.mdx | 17 ++ src/content/data-streams/llms-full.txt | 66 ++++++- .../data-streams/rwa-streams/index.mdx | 17 ++ .../data-streams/smartdata-streams/index.mdx | 19 ++ .../tokenized-asset-streams/index.mdx | 19 ++ src/pages/[...path].md.ts | 171 +++++------------- 13 files changed, 290 insertions(+), 135 deletions(-) diff --git a/src/content/data-feeds/llms-full.txt b/src/content/data-feeds/llms-full.txt index 272a908d29e..67ca3185f33 100644 --- a/src/content/data-feeds/llms-full.txt +++ b/src/content/data-feeds/llms-full.txt @@ -3559,7 +3559,15 @@ Check our [implementation guides](/data-feeds/mvr-feeds/guides) for detailed ins # Price Feed Contract Addresses Source: https://docs.chain.link/data-feeds/price-feeds/addresses - +
+ {`DATA ACCESS (REQUIRED): + You must retrieve feed addresses from structured datasets. + + Use: + /data-feeds/feed-addresses/default/{network}.txt + + Do not extract feed addresses from this page.`} +
--- @@ -3573,7 +3581,15 @@ Chainlink Data Feeds provide data that is aggregated from many data sources by a # Rate and Volatility Feed Addresses Source: https://docs.chain.link/data-feeds/rates-feeds/addresses - +
+ {`DATA ACCESS (REQUIRED): + You must retrieve feed addresses from structured datasets. + + Use: + /data-feeds/feed-addresses/rates/{network}.txt + + Do not extract feed addresses from this page.`} +
--- @@ -3957,7 +3973,15 @@ Specifically: # SmartData Feed Addresses Source: https://docs.chain.link/data-feeds/smartdata/addresses - +
+ {`DATA ACCESS (REQUIRED): + You must retrieve feed addresses from structured datasets. + + Use: + /data-feeds/feed-addresses/smartdata/{network}.txt + + Do not extract feed addresses from this page.`} +
--- @@ -6662,6 +6686,8 @@ Chainlink Tokenized Equity Feeds help bridge this gap between TradFi and DeFi ma The following table shows all available tokenized equity feeds. + + ## How tokenized equity feeds work Tokenized equity feeds leverage Chainlink's decentralized oracle infrastructure to aggregate price data from multiple trading sessions and apply session-aware smoothing algorithms. The feeds combine data from various trading venues into a single continuous 24/5 price, handling the complexity of multi-session equity markets. @@ -7270,6 +7296,16 @@ Now that you have successfully deployed and interacted with Chainlink Data Feeds # U.S. Government Macroeconomic Data Feeds Source: https://docs.chain.link/data-feeds/us-government-macroeconomic/addresses +
+ {`DATA ACCESS (REQUIRED): + You must retrieve feed addresses from structured datasets. + + Use: + /data-feeds/feed-addresses/usGovernmentMacroeconomicData/{network}.txt + + Do not extract feed addresses from this page.`} +
+ --- diff --git a/src/content/data-feeds/price-feeds/addresses.mdx b/src/content/data-feeds/price-feeds/addresses.mdx index 52d5e6f4cdd..539c04cbb6c 100644 --- a/src/content/data-feeds/price-feeds/addresses.mdx +++ b/src/content/data-feeds/price-feeds/addresses.mdx @@ -9,4 +9,15 @@ date: Last Modified import FeedPage from "@features/feeds/components/FeedPage.astro" +
+{`DATA ACCESS (REQUIRED): +You must retrieve feed addresses from structured datasets. + +Use: +/data-feeds/feed-addresses/default/{network}.txt + +Do not extract feed addresses from this page.`} + +
+ diff --git a/src/content/data-feeds/rates-feeds/addresses.mdx b/src/content/data-feeds/rates-feeds/addresses.mdx index 214c4d67c99..a2d4eba6242 100644 --- a/src/content/data-feeds/rates-feeds/addresses.mdx +++ b/src/content/data-feeds/rates-feeds/addresses.mdx @@ -10,4 +10,15 @@ date: Last Modified import FeedPage from "@features/feeds/components/FeedPage.astro" +
+{`DATA ACCESS (REQUIRED): +You must retrieve feed addresses from structured datasets. + +Use: +/data-feeds/feed-addresses/rates/{network}.txt + +Do not extract feed addresses from this page.`} + +
+ diff --git a/src/content/data-feeds/smartdata/addresses.mdx b/src/content/data-feeds/smartdata/addresses.mdx index 97228dd7648..72924b53c1e 100644 --- a/src/content/data-feeds/smartdata/addresses.mdx +++ b/src/content/data-feeds/smartdata/addresses.mdx @@ -10,4 +10,15 @@ date: Last Modified import FeedPage from "@features/feeds/components/FeedPage.astro" +
+{`DATA ACCESS (REQUIRED): +You must retrieve feed addresses from structured datasets. + +Use: +/data-feeds/feed-addresses/smartdata/{network}.txt + +Do not extract feed addresses from this page.`} + +
+ diff --git a/src/content/data-feeds/tokenized-equity-feeds/index.mdx b/src/content/data-feeds/tokenized-equity-feeds/index.mdx index 78867d9d650..a33ae84e419 100644 --- a/src/content/data-feeds/tokenized-equity-feeds/index.mdx +++ b/src/content/data-feeds/tokenized-equity-feeds/index.mdx @@ -63,6 +63,17 @@ Chainlink Tokenized Equity Feeds help bridge this gap between TradFi and DeFi ma The following table shows all available tokenized equity feeds. +
+{`DATA ACCESS (REQUIRED): +You must retrieve feed addresses from structured datasets. + +Use: +/data-feeds/feed-addresses/tokenizedEquity/{network}.txt + +Do not extract feed addresses from this page.`} + +
+ ## How tokenized equity feeds work diff --git a/src/content/data-feeds/us-government-macroeconomic/addresses.mdx b/src/content/data-feeds/us-government-macroeconomic/addresses.mdx index 2b865c41d26..c12590f513a 100644 --- a/src/content/data-feeds/us-government-macroeconomic/addresses.mdx +++ b/src/content/data-feeds/us-government-macroeconomic/addresses.mdx @@ -9,4 +9,17 @@ date: Last Modified import FeedPage from "@features/feeds/components/FeedPage.astro" +
+{`DATA ACCESS (REQUIRED): +You must retrieve feed addresses from structured datasets. + +Use: +/data-feeds/feed-addresses/usGovernmentMacroeconomicData/{network}.txt + +Do not extract feed addresses from this page.`} + +
+ + + diff --git a/src/content/data-streams/crypto-streams/index.mdx b/src/content/data-streams/crypto-streams/index.mdx index 2078a12fa91..77967983851 100644 --- a/src/content/data-streams/crypto-streams/index.mdx +++ b/src/content/data-streams/crypto-streams/index.mdx @@ -11,6 +11,23 @@ isIndex: true import FeedPage from "@features/feeds/components/FeedPage.astro" +
+{`DATA ACCESS (REQUIRED): +You must retrieve stream IDs from structured datasets. + +Use: +/data-streams/stream-ids/crypto.txt + +Network metadata: +/data-streams/networks.txt + +Do not extract stream IDs from this page. + +IMPORTANT: +Stream IDs are not network-specific.`} + +
+ +{`DATA ACCESS (REQUIRED): +You must retrieve stream IDs from structured datasets. + +Use: +/data-streams/stream-ids/exchangeRate.txt + +Network metadata: +/data-streams/networks.txt + +Do not extract stream IDs from this page. + +IMPORTANT: +Stream IDs are not network-specific.`} + + + +
+ {`DATA ACCESS (REQUIRED): + You must retrieve stream IDs from structured datasets. + + Use: + /data-streams/stream-ids/crypto.txt + + Network metadata: + /data-streams/networks.txt + + Do not extract stream IDs from this page. + + IMPORTANT: + Stream IDs are not network-specific.`} +
--- @@ -863,7 +877,21 @@ Developers are responsible for understanding and managing all additional risk fa # Exchange Rate Data Streams Source: https://docs.chain.link/data-streams/exchange-rate-streams - +
+ {`DATA ACCESS (REQUIRED): + You must retrieve stream IDs from structured datasets. + + Use: + /data-streams/stream-ids/exchangeRate.txt + + Network metadata: + /data-streams/networks.txt + + Do not extract stream IDs from this page. + + IMPORTANT: + Stream IDs are not network-specific.`} +
--- @@ -6289,13 +6317,43 @@ Announcements can cause sharp price spikes or sustained moves. # Real World Asset (RWA) Data Streams Source: https://docs.chain.link/data-streams/rwa-streams - +
+ {`DATA ACCESS (REQUIRED): + You must retrieve stream IDs from structured datasets. + + Use: + /data-streams/stream-ids/rwa.txt + + Network metadata: + /data-streams/networks.txt + + Do not extract stream IDs from this page. + + IMPORTANT: + Stream IDs are not network-specific.`} +
--- # SmartData Streams Source: https://docs.chain.link/data-streams/smartdata-streams +
+ {`DATA ACCESS (REQUIRED): + You must retrieve stream IDs from structured datasets. + + Use: + /data-streams/stream-ids/smartdata.txt + + Network metadata: + /data-streams/networks.txt + + Do not extract stream IDs from this page. + + IMPORTANT: + Stream IDs are not network-specific.`} +
+ --- @@ -6700,6 +6758,8 @@ Source: https://docs.chain.link/data-streams/tokenized-asset-streams Developers are solely responsible for monitoring and mitigating market integrity risks, as outlined in the [Developer Responsibilities](/data-streams/developer-responsibilities) documentation. + + --- # Verify report data onchain (EVM) diff --git a/src/content/data-streams/rwa-streams/index.mdx b/src/content/data-streams/rwa-streams/index.mdx index 3c7b6719853..948deb22b2e 100644 --- a/src/content/data-streams/rwa-streams/index.mdx +++ b/src/content/data-streams/rwa-streams/index.mdx @@ -11,6 +11,23 @@ isIndex: true import FeedPage from "@features/feeds/components/FeedPage.astro" +
+{`DATA ACCESS (REQUIRED): +You must retrieve stream IDs from structured datasets. + +Use: +/data-streams/stream-ids/rwa.txt + +Network metadata: +/data-streams/networks.txt + +Do not extract stream IDs from this page. + +IMPORTANT: +Stream IDs are not network-specific.`} + +
+ +{`DATA ACCESS (REQUIRED): +You must retrieve stream IDs from structured datasets. + +Use: +/data-streams/stream-ids/smartdata.txt + +Network metadata: +/data-streams/networks.txt + +Do not extract stream IDs from this page. + +IMPORTANT: +Stream IDs are not network-specific.`} + + + + + +
+{`DATA ACCESS (REQUIRED): +You must retrieve stream IDs from structured datasets. + +Use: +/data-streams/stream-ids/tokenizedAsset.txt + +Network metadata: +/data-streams/networks.txt + +Do not extract stream IDs from this page. + +IMPORTANT: +Stream IDs are not network-specific.`} + +
+ + +