Futures
Futures streams provide price data for exchange-traded futures contracts. These streams enable onchain protocols to build markets around futures price exposure. Coverage currently includes commodities — energy and precious metals — described below.
Because futures contracts expire, Data Streams does not publish one feed per contract. Instead, each instrument is represented by three generic feeds that always track the current front, second, and third contract. This is explained in Rotation methodology below.
Developers are responsible for choosing the appropriate feed and ensuring that the operation and performance of their choice matches expectations. For more information, see the Developer Responsibilities guidance.
Schema
Futures streams use the Futures (v14) schema.
The v14 schema includes consensus mid, bid, and ask prices, contract expiration and first day of notice, the tracked contract month, market status, and a staleness measure. It also includes continuous roll price fields — goldmanRollPrice, interpolatedGoldmanRollPrice, and currentBusinessDay — described below.
Continuous roll price fields
In addition to the raw generic feed prices above — which switch entirely to the next contract at the first notice date — Chainlink also publishes continuous price series built using alternative rolling methodologies, for integrators who want a smoothed price instead of a hard cutover:
| Field | Description |
|---|---|
goldmanRollPrice | Uses the Goldman Roll (GSCI methodology): shifts 20% of the position to the next contract each business day, over business days 5–9 of the month preceding delivery |
interpolatedGoldmanRollPrice | Interpolated Goldman Roll price. Reported as 0 when the feed is not within an active roll window |
currentBusinessDay | Current business day count within the active roll window, so integrators can implement the Goldman Roll methodology themselves |
For the complete field list and definitions, see the Futures (v14) schema.
Implementation
Futures streams are consumed like any other Data Streams report: you fetch the latest report for a feed ID and decode it with the SDK. The only difference is the report schema version, which is v14 for futures streams.
The examples below fetch and decode a live NY-UKOIL/USD futures report from the testnet API. Each SDK auto-detects or explicitly targets the v14 schema.
// FuturesFetch.go — Fetch and decode a Futures (v14) report with the Go SDK.
//
// Usage:
// export API_KEY="..."
// export API_SECRET="..."
// go run FuturesFetch.go 0x000ebfe7b7560e1866ef91d4607ce5ee7474382794e5ec755f38b70b1f30e647
package main
import (
"context"
"fmt"
"os"
"time"
streams "github.com/smartcontractkit/data-streams-sdk/go/v2"
feed "github.com/smartcontractkit/data-streams-sdk/go/v2/feed"
report "github.com/smartcontractkit/data-streams-sdk/go/v2/report"
v14 "github.com/smartcontractkit/data-streams-sdk/go/v2/report/v14"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: go run FuturesFetch.go [FeedID]\n")
os.Exit(1)
}
feedIDInput := os.Args[1]
apiKey := os.Getenv("API_KEY")
apiSecret := os.Getenv("API_SECRET")
if apiKey == "" || apiSecret == "" {
fmt.Fprintf(os.Stderr, "API_KEY and API_SECRET environment variables must be set\n")
os.Exit(1)
}
cfg := streams.Config{
ApiKey: apiKey,
ApiSecret: apiSecret,
RestURL: "https://api.testnet-dataengine.chain.link",
Logger: streams.LogPrintf,
}
client, err := streams.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
var feedID feed.ID
if err := feedID.FromString(feedIDInput); err != nil {
fmt.Fprintf(os.Stderr, "Invalid feed ID format '%s': %v\n", feedIDInput, err)
os.Exit(1)
}
reportResponse, err := client.GetLatestReport(ctx, feedID)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get latest report: %v\n", err)
os.Exit(1)
}
// Decode the v14 (Futures) report
decodedReport, err := report.Decode[v14.Data](reportResponse.FullReport)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to decode report: %v\n", err)
os.Exit(1)
}
d := decodedReport.Data
fmt.Printf("\nDecoded V14 Report for Stream ID %s:\n", feedIDInput)
fmt.Println("------------------------------------------")
fmt.Printf("Mid Price : %s\n", d.MidPrice.String())
fmt.Printf("Bid Price : %s\n", d.BidPrice.String())
fmt.Printf("Ask Price : %s\n", d.AskPrice.String())
fmt.Printf("Expiry Time : %d\n", d.ExpiryTime.Unix())
fmt.Printf("First Day of Notice : %d\n", d.FirstDayOfNotice.Unix())
fmt.Printf("Last Seen TimestampNs : %d\n", d.LastSeenTimestampNs.UnixNano())
fmt.Printf("Market Status : %d\n", d.MarketStatus)
fmt.Printf("Contract Month : %d\n", d.ContractMonth)
fmt.Printf("Goldman Roll Price : %s\n", d.GoldmanRollPrice.String())
fmt.Printf("Current Business Day : %d\n", d.CurrentBusinessDay)
fmt.Printf("Interp Goldman Roll : %s\n", d.InterpolatedGoldmanRollPrice.String())
fmt.Println("------------------------------------------")
}
// FuturesFetch.ts — Fetch and decode a Futures (v14) report with the TypeScript SDK.
//
// Usage:
// export API_KEY="..."
// export USER_SECRET="..."
// npx tsx FuturesFetch.ts 0x000ebfe7b7560e1866ef91d4607ce5ee7474382794e5ec755f38b70b1f30e647
import { createClient, decodeReport, getReportVersion, formatReport, LogLevel } from "@chainlink/data-streams-sdk"
import "dotenv/config"
async function main() {
if (process.argv.length < 3) {
console.error("Please provide a feed ID as an argument")
process.exit(1)
}
const feedId = process.argv[2]
const version = getReportVersion(feedId)
const config = {
apiKey: process.env.API_KEY || "YOUR_API_KEY",
userSecret: process.env.USER_SECRET || "YOUR_USER_SECRET",
endpoint: "https://api.testnet-dataengine.chain.link",
wsEndpoint: "wss://ws.testnet-dataengine.chain.link",
logging: {
logger: console,
logLevel: LogLevel.INFO,
},
}
const client = createClient(config)
console.log(`\nFetching latest report for feed ${feedId} (${version})...\n`)
const report = await client.getLatestReport(feedId)
console.log(`Raw Report Blob: ${report.fullReport}`)
// The TS SDK auto-detects the report version from the feed ID.
const decodedData = decodeReport(report.fullReport, report.feedID)
const decodedReport = {
...decodedData,
feedID: report.feedID,
validFromTimestamp: report.validFromTimestamp,
observationsTimestamp: report.observationsTimestamp,
}
console.log(formatReport(decodedReport, version))
}
main().catch((error) => {
console.error("Error:", error instanceof Error ? error.message : error)
process.exit(1)
})
// FuturesFetch.rs — Fetch and decode a Futures (v14) report with the Rust SDK.
//
// Usage:
// export API_KEY="..."
// export API_SECRET="..."
// cargo run 0x000ebfe7b7560e1866ef91d4607ce5ee7474382794e5ec755f38b70b1f30e647
use chainlink_data_streams_report::feed_id::ID;
use chainlink_data_streams_report::report::{ decode_full_report, v14::ReportDataV14 };
use chainlink_data_streams_sdk::client::Client;
use chainlink_data_streams_sdk::config::Config;
use std::env;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: cargo run [FeedID]");
std::process::exit(1);
}
let feed_id_input = &args[1];
let api_key = env::var("API_KEY").expect("API_KEY must be set");
let api_secret = env::var("API_SECRET").expect("API_SECRET must be set");
let config = Config::new(
api_key,
api_secret,
"https://api.testnet-dataengine.chain.link".to_string(),
"wss://api.testnet-dataengine.chain.link/ws".to_string(),
)
.build()?;
let client = Client::new(config)?;
let feed_id = ID::from_hex_str(feed_id_input)?;
let response = client.get_latest_report(feed_id).await?;
let full_report = hex::decode(&response.report.full_report[2..])?;
let (_report_context, report_blob) = decode_full_report(&full_report)?;
let report_data = ReportDataV14::decode(&report_blob)?;
println!("\nDecoded V14 Report for Stream ID {}:", feed_id_input);
println!("------------------------------------------");
println!("Mid Price : {}", report_data.mid_price);
println!("Bid Price : {}", report_data.bid_price);
println!("Ask Price : {}", report_data.ask_price);
println!("Expiry Time : {}", report_data.expiry_time);
println!("First Day of Notice : {}", report_data.first_day_of_notice);
println!("Last Seen TimestampNs : {}", report_data.last_seen_timestamp_ns);
println!("Market Status : {}", report_data.market_status);
println!("Contract Month : {}", report_data.contract_month);
println!("Goldman Roll Price : {}", report_data.goldman_roll_price);
println!("Current Business Day : {}", report_data.current_business_day);
println!("Interp Goldman Roll : {}", report_data.interpolated_goldman_roll_price);
println!("------------------------------------------");
Ok(())
}
Expected output
Running any of the examples above against a live NY-UKOIL/USD futures feed produces a decoded report similar to the following (values are illustrative and change with each report):
Decoded V14 Report for Stream ID 0x000ebfe7b7560e1866ef91d4607ce5ee7474382794e5ec755f38b70b1f30e647:
------------------------------------------
Valid From Timestamp : 1789593821
Observations Timestamp: 1789593821
Native Fee : 132954556310247
LINK Fee : 29292627326579709
Expires At : 1792185821
Mid Price : 105584000000000000000
Bid Price : 105539000000000000000
Ask Price : 105629000000000000000
Expiry Time : 1790740800
First Day of Notice : 1790740800
Last Seen TimestampNs : 1789592399813000000
Market Status : 1
Contract Month : 11
Goldman Roll Price : 100574999999999990000
Current Business Day : 11
Interp Goldman Roll : 0
------------------------------------------
The prices above are 18-decimal fixed-point integers. For example, Mid Price 105584000000000000000 equals 105.584 USD. Contract Month 11 is November, and Market Status 1 means the market is closed.
Decoded fields
The v14 report decodes to the following fields. Prices are 18-decimal fixed-point integers; timestamps are Unix epoch seconds unless noted.
| Field | Type | Meaning |
|---|---|---|
midPrice, bidPrice, askPrice | int192 | Consensus mid, bid, and ask prices (18 decimals) |
expiryTime | uint64 | Contract expiration, Unix seconds |
firstDayOfNotice | uint64 | First day of notice, Unix seconds |
lastSeenTimestampNs | uint64 | Last provider update, nanoseconds |
marketStatus | uint32 | Market status: 0 Unknown, 1 Closed, 2 Open |
contractMonth | uint32 | Contract month, 1 (Jan) to 12 (Dec) |
goldmanRollPrice | int192 | Goldman Roll continuous price (18 decimals) |
currentBusinessDay | uint32 | Current business day in the roll window |
interpolatedGoldmanRollPrice | int192 | Interpolated Goldman Roll price; 0 outside a roll window |
Implementation patterns
Futures streams differ from spot streams in a few ways that affect how you consume them:
- Use
marketStatus, not timestamps, to gate trading.marketStatusis the authoritative signal for whether the market is open. Do not infer market state fromlastSeenTimestampNsor other timestamps; they only indicate when data was last recorded. See Market Hours for the value mapping per feed. - Monitor
lastSeenTimestampNsfor staleness. Futures are sourced from a single provider per feed. If updates stop arriving, the price can go stale without an obvious price change. Pause trading or restrict liquidations when the timestamp lags your freshness threshold. - Expect a price discontinuity at each roll. When the front contract expires, the feed re-maps to the next contract, which can trade at a different price. Do not treat this step as a data error. If your application needs a smooth series, build it from the continuous roll fields (
goldmanRollPrice,interpolatedGoldmanRollPrice) or construct your own off-chain. - Do not hardcode contract-month spacing. The gap between the front, second, and third feeds varies by instrument (one month for energy, up to a few months for precious metals). Read
contractMonthfrom the report rather than assuming monthly spacing. - Roll early if you are expiry-sensitive. In the final days of a contract, liquidity migrates to the next contract while the front feed still tracks the expiring one, which can increase volatility. If your application is sensitive to this, roll ahead of expiry using the second feed.
Consuming the three generic feeds
Each instrument is exposed as three feeds that always track the front, second, and third eligible contracts. The three feeds are the same schema (v14) and differ only in which contract they currently reference. To consume the curve correctly:
- Identify the cut from
contractMonthandexpiryTime, not from the feed ID. The feed IDs are static and do not encode which cut a feed is. ReadcontractMonthandexpiryTimefrom each report to know which contract a feed currently tracks. The feed with the nearestexpiryTimeis the front; the next two are the second and third. - The three feeds cascade on the front contract's expiry. When the front expires, the front feed takes over the second feed's contract, the second takes over the third's, and the third picks up the next eligible contract. Your integration should not hardcode which contract a feed tracks; it should re-read
contractMonth/expiryTimeeach report. - Build your own roll strategy. Data Streams does not impose one. You can hold the front feed to expiry, roll early to the second feed when liquidity migrates, or construct a continuous series off-chain. The three feeds give you the visibility to implement whichever you choose.
The example below fetches all three feeds for an instrument and prints which contract each currently tracks, so you can see the curve and detect a roll:
// Read the front, second, and third feeds for an instrument.
// Replace the three feed IDs with the front/second/third feeds for your instrument.
feedIDs := []feed.ID{frontID, secondID, thirdID}
for _, id := range feedIDs {
resp, err := client.GetLatestReport(ctx, id)
if err != nil {
log.Fatalf("fetch %s: %v", id, err)
}
decoded, err := report.Decode[v14.Data](resp.FullReport)
if err != nil {
log.Fatalf("decode %s: %v", id, err)
}
d := decoded.Data
fmt.Printf("%s -> contract month %d, expiry %d, mid %s\n",
id, d.ContractMonth, d.ExpiryTime.Unix(), d.MidPrice.String())
}
// Read the front, second, and third feeds for an instrument.
// Replace the three feed IDs with the front/second/third feeds for your instrument.
const feedIds = [frontId, secondId, thirdId]
for (const id of feedIds) {
const report = await client.getLatestReport(id)
const decoded = decodeReport(report.fullReport, report.feedID) as any
console.log(`${id} -> contract month ${decoded.contractMonth}, expiry ${decoded.expiryTime}, mid ${decoded.midPrice}`)
}
// Read the front, second, and third feeds for an instrument.
// Replace the three feed IDs with the front/second/third feeds for your instrument.
let feed_ids = [front_id, second_id, third_id];
for id in feed_ids {
let resp = client.get_latest_report(id).await?;
let full_report = hex::decode(&resp.report.full_report[2..])?;
let (_ctx, blob) = decode_full_report(&full_report)?;
let d = ReportDataV14::decode(&blob)?;
println!("{} -> contract month {}, expiry {}, mid {}",
id, d.contract_month, d.expiry_time, d.mid_price);
}
Notes
- Prices are 18-decimal fixed-point integers.
midPrice,bidPrice,askPrice,goldmanRollPrice, andinterpolatedGoldmanRollPriceare raw integers. Divide by10^18to get the human-readable price. In Go and Rust these are big-number types; call.String()to display them. interpolatedGoldmanRollPriceis0outside a roll window. Do not treat a0value as a price of zero; it means the feed is not currently in an active roll window.contractMonthis validated to1–12. The SDKs reject reports with an out-of-range month, so a successful decode guarantees a valid month.- The feed ID encodes the schema version. The TypeScript SDK's
getReportVersionand the Go SDK'sfeedID.Version()derive the schema from the feed ID. Use the v14 decoder for futures feeds.
Coverage
Futures streams are currently available for the following commodities. Additional commodities and asset classes may be added based on demand.
| Commodity | Generic feeds |
|---|---|
| Gold | Front, second, and third feeds |
| Silver | Front, second, and third feeds |
| US Oil | Front, second, and third feeds |
| UK Oil | Front, second, and third feeds |
| Natural Gas | Front, second, and third feeds |
Naming and price representation
Feed names use the plain instrument name only (for example, "US Oil," "Gold," "Natural Gas").
Contract specifications — contract size, tick size, quotation units, and similar terms — can vary between exchanges for the same underlying instrument. Chainlink does not publish these specifications, since they belong to the exchange where a given contract is listed. If your application needs exact contract specifications, consult that exchange's own public product documentation directly.
Rotation methodology
Unlike spot instruments, futures contracts have a finite life. For example, a given month's Natural Gas contract trades for a limited window and then expires. If Data Streams published a dedicated feed per contract, integrators would need to re-point their integration to a new feed every time a contract expired.
To avoid this, each instrument is represented by three static, non-expiring generic feeds that always represent the near-term forward curve of eligible contracts:
| Generic feed | Meaning |
|---|---|
| Front feed | Tracks the nearest unexpired eligible contract |
| Second feed | Tracks the next eligible contract after the front contract |
| Third feed | Tracks the eligible contract after that |
This design means:
- Zero maintenance. Integrate the three generic feeds once. You never need to re-point feed endpoints when a contract expires — the feeds re-map internally.
- Full visibility of the curve. Because the front, second, and third contracts are exposed simultaneously, you retain full autonomy over your own rolling logic. You can roll positions on a fixed schedule ahead of expiry, or when liquidity migrates from the front contract to the second — Data Streams does not impose a roll strategy.
How the roll works
When the front contract reaches its expiration, the generic feeds cascade forward:
- The front feed takes over the contract previously tracked by the second feed
- The second feed takes over the contract previously tracked by the third feed
- The third feed begins tracking the next eligible contract in that instrument's cycle
A few properties of the roll are worth calling out explicitly:
- The roll is triggered by the front contract's expiration, as defined by the listed expiration calendar for that specific instrument. Expiration conventions differ by instrument, so rolls for different instruments happen on different dates — there is no single "roll day" across all instruments.
- The switch happens right after the primary exchange's trading session closes on the expiration date, not at a fixed clock time across all instruments. For these commodities, that session close is typically around 17:00 ET.
- The roll is a re-mapping, not a price adjustment. At the moment of the roll, the front feed switches from quoting the expiring contract to quoting what was previously the second contract. Because different contract months can trade at different prices, expect a price discontinuity on the generic feed at each roll. Feeds are not back-adjusted into a smooth continuous series — if your application needs a continuous series, construct it from the raw generic feeds on your end.
- Until expiration, the front feed continues to track the expiring contract, even in its final days when liquidity has typically already migrated to the next contract. This is precisely why the second feed is exposed — so you can implement your own liquidity-based rolls ahead of expiry instead of holding the front contract to the end.
Contract eligibility varies by instrument
The front feed does not necessarily track the nearest calendar month — it tracks the nearest unexpired eligible contract, whatever month that happens to be. Each instrument has its own set of listed contract months, and for several instruments, the actively traded contracts are only a subset of what is listed:
- Energy (US Oil, UK Oil, Natural Gas): every calendar month is listed, though liquidity is generally concentrated in nearer maturities. The generic feeds walk the full monthly strip regardless, so the front, second, and third feeds track three consecutive calendar months.
- Precious metals (Gold, Silver): many calendar months are listed, but liquidity concentrates in a benchmark cycle. The generic feeds follow the benchmark cycle only and skip non-benchmark months, even though those contracts exist and trade.
| Commodity | Benchmark months |
|---|---|
| Gold | Feb, Apr, Jun, Aug, Oct, Dec |
| Silver | Mar, May, Jul, Sep, Dec |
The practical consequence: the gap between the front, second, and third feeds is not always one month. For Gold or Silver, consecutive generic feeds can be one to three months apart. Do not assume monthly spacing in your integration.
Worked example
Gold follows its benchmark cycle (Feb/Apr/Jun/Aug/Oct/Dec). In August 2026, before the August contract expires:
| Feed | Contract tracked |
|---|---|
| Front feed | August 2026 |
| Second feed | October 2026 |
| Third feed | December 2026 |
When the August contract expires, the front feed jumps directly to October, the second feed jumps to December, and the third feed begins tracking February 2027 — skipping the non-benchmark September and November contracts entirely, since they never appear on the generic feeds.
Integration guidance
- Do not hardcode contract-month assumptions. The spacing between the front, second, and third feeds varies by instrument — one month for energy, up to a few months for precious metals.
- Expect price steps at rolls. A protocol using the front feed directly for margining or liquidation should either accept the step as economically correct (the front contract genuinely changed), implement its own early roll using the second feed, or build an adjusted continuous series off-chain from the raw generic feeds.
- Roll early to avoid volatility near expiry. In the final days of a contract's life, trading volume typically migrates to the next contract while the front feed still tracks the expiring one. Thin liquidity in the expiring contract can increase price volatility, which may unexpectedly trigger perpetual trades, liquidations, or other price-sensitive logic. If your application is sensitive to this, consider rolling ahead of expiry using the second feed.
- Follow the listed expiration calendar. Expiration dates follow the official listed calendar of the exchange where the underlying contract trades, including holiday adjustments. Consult that exchange for the operative source of truth.
Trading hours
Futures trade during standard trading hours for each instrument, with a scheduled break each trading day. Consult the exchange where the underlying contract is listed for holiday and early-close schedules.
Each feed's marketStatus field reflects the current market status of the primary exchange venue for that instrument.
Risk considerations
Integrating protocols are responsible for implementing appropriate monitoring and risk mitigation mechanisms to ensure safe market operation.
Single-source data
Each Futures instrument is sourced from a single institutional-grade data provider per feed. While this enables high-frequency, low-latency delivery, it introduces dependency on that provider's data quality and availability. There is no redundant cross-validation between providers for a given feed.
If the underlying provider fails to deliver data, there is no fallback source, and a gap in data may not be immediately obvious from price alone. Protocols should continuously monitor feed freshness and implement safeguards — such as pausing trading or restricting liquidations — if updates stop arriving or a feed appears stale. Review the Developer Responsibilities guidance before integration.
Available streams
The table below shows all available Futures streams.
Mainnet Futures Streams
No Mainnet feeds available.
Support
For inquiries related to a data outage, contact Chainlink Labs at chainlink_data_feeds@smartcontract.com.