Skip to content
Velo

Builder and Query

Requesting market data is two steps:

  1. Describe the request: with the fluent builder on the futures, options, and spot namespaces, or as a typed request object.
  2. Execute it: with velo.query() to collect all the data, velo.stream() to receive rows one at a time, or velo.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',
    }),
);

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:

CategoryStepPurpose
Data selectorsprice(), volume(), fundingRate(), …Choose the columns. Call as many as needed; they accumulate
Market scopefor(scope)Choose the markets: exchanges plus exactly one of coins or products
Time scopeover(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

CallResult
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',
    }),
);
  • between describes a range [begin, end), where begin is included and end is excluded. It accepts Date objects or millisecond timestamps.
  • last accepts a duration in minutes, hours, days, or weeks. For example: 30m, 2h, 3D, and 1W. 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-empty coins or products selection. Every exchange for the market is selected when exchanges is 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.