Builder and Query
Requesting market data is two steps:
- Describe the request: with the fluent builder on the
futures,options, andspotnamespaces, or as a typed request object. - Execute it: with
velo.query()to collect all the data,velo.stream()to receive rows one at a time, orvelo.watch()to subscribe to a live feed.
A request sends nothing on its own, so it can be built once, reused, and passed around. Both ways of describing a request go through the same validation, request-splitting, and decoding pipeline, and return the same typed Data object.
Example
These two examples request the same futures columns over the same time range:
const end = Date.now();
const begin = end - 60 * 60 * 1_000;
const data = await velo.query(
futures
.price(['close'])
.openInterest(['close'])
.volume(['total'])
.for({
exchanges: ['binance-futures', 'bybit'],
coins: ['BTC'],
})
.over({
between: [begin, end],
resolution: '1m',
}),
);const end = Date.now();
const begin = end - 60 * 60 * 1_000;
const data = await velo.query({
kind: 'futures.rows',
params: {
exchanges: ['binance-futures', 'bybit'],
coins: ['BTC'],
columns: [
'close_price',
'dollar_open_interest_close',
'dollar_volume',
],
begin,
end,
resolution: '1m',
},
});The builder translates semantic selectors into raw API columns. For example, openInterest(['close']) selects dollar_open_interest_close when sending the HTTP request.
Describing a request
A builder is made of three kinds of step, and for() and over() may be called in either order:
| Category | Step | Purpose |
|---|---|---|
| Data selectors | price(), volume(), fundingRate(), … | Choose the columns. Call as many as needed; they accumulate |
| Market scope | for(scope) | Choose the markets: exchanges plus exactly one of coins or products |
| Time scope | over(scope) | Choose the time window: exactly one of between or last, plus a resolution |
Both for() and over() are required. Attempting to execute the request before calling both results in a compile-time error that identifies the missing method.
Because a request is a value, you can share it before it is fully configured:
const btc = futures
.price(['close'])
.volume(['total'])
.for({ exchanges: ['bybit'], coins: ['BTC'] });
const hourly = await velo.query(btc.over({ last: '7D', resolution: '1h' }));
const daily = await velo.query(btc.over({ last: '90D', resolution: '1D' }));Executing a request
| Call | Result |
|---|---|
velo.query(request) | Executes every request and resolves to Data |
velo.stream(request) | Executes and yields decoded rows one at a time |
velo.watch(request) | Opens a live subscription and returns a watcher |
Use velo.query() for the common case:
const data = await velo.query(
futures
.price()
.for({ products: ['BTCUSDT'] })
.over({ last: '1h', resolution: '1m' }),
);Use velo.stream() to process rows without holding the whole result in memory:
for await (const row of velo.stream(request)) {
console.log(row.close_price);
}Each call executes the request once, so querying and streaming the same request sends its requests twice. Read more at Stream Large Queries.
Time ranges
over() accepts an explicit or a trailing range:
const data = await velo.query(
futures
.price(['close'])
.for({ exchanges: ['binance-futures'], coins: ['BTC'] })
.over({
between: [
new Date('2026-01-01T00:00:00Z'),
new Date('2026-02-20T00:00:00Z'),
],
resolution: '4h',
}),
);const data = await velo.query(
futures
.price(['close'])
.for({ exchanges: ['binance-futures'], coins: ['BTC'] })
.over({ last: '1h', resolution: '1m' }),
);betweendescribes a range[begin, end), wherebeginis included andendis excluded. It acceptsDateobjects or millisecond timestamps.lastaccepts a duration in minutes, hours, days, or weeks. For example:30m,2h,3D, and1W. It is anchored when the request is executed.
Request validation
A market-data request must include:
- At least one data column.
- A
for()step with exactly one non-emptycoinsorproductsselection. Every exchange for the market is selected whenexchangesis omitted. - An
over()step with a valid time range and resolution.
Most of this is enforced by the types, so an invalid request will not compile. Values that cannot be checked at compile time, such as symbol names, are validated when the request is built, before any HTTP request is sent.