Guide · TypeScript

Use Chart Library with the Vercel AI SDK

Most agent-grounding guides are Python. This one is TypeScript. The Vercel AI SDK is the default agent stack on Next.js / Node, and Chart Library’s core loop is a plain HTTP call — so a drop-in tool is about fifteen lines, with no API key for the free keyless loop.

Your agent stops inventing technical analysis and starts citing the cohort of historical analogs: the forward-return distribution, a calibrated band, and the drivers — base rates, never a price forecast.

Step 1: Install

npm i ai @ai-sdk/openai zod

ai is the Vercel AI SDK, @ai-sdk/openai the model provider (swap for @ai-sdk/anthropic or any other), and zod describes the tool input. Nothing Chart Library–specific to install — the tool is just a fetch.

Step 2: Define the keyless tool

The whole integration is one tool(). It posts a (symbol, date, timeframe) subject to the public cohort_analyze endpoint and hands the model a trimmed, decision-ready result. No key — the core loop is free and keyless (add one only past 1,000 calls/day).

// chart-library-tool.ts
import { tool } from "ai";
import { z } from "zod";

export const pullComps = tool({
  description:
    "Pull the cohort of historical analogs for a stock setup (symbol, date, " +
    "timeframe). Returns the forward-return distribution, a calibrated 80% band, " +
    "and the drivers that separated winners from losers. Historical base rates, " +
    "never a price forecast.",
  inputSchema: z.object({
    symbol: z.string().describe("Ticker, e.g. NVDA"),
    date: z.string().describe("Setup date, YYYY-MM-DD"),
    timeframe: z.enum(["5m", "15m", "30m", "1h", "1d"]).default("1d"),
  }),
  execute: async ({ symbol, date, timeframe }) => {
    const res = await fetch("https://chartlibrary.io/api/v1/cohort_analyze", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      // No Authorization header — the core loop is free + keyless.
      body: JSON.stringify({
        anchor: { symbol, date, timeframe },
        horizons: [1, 5, 10],
      }),
    });
    if (!res.ok) return { error: `Chart Library returned ${res.status}` };
    const d = await res.json();

    const horizons = Object.fromEntries(
      [1, 5, 10].map((h) => {
        const o = d.outcome_distribution?.[h] ?? {};
        const c = d.calibration_by_horizon?.[`${h}d`] ?? {};
        return [
          `${h}d`,
          {
            median_pct: o.median,
            up_rate: o.win_rate,
            cohort_p10_p90: [o.p10, o.p90],
            calibrated_80_band: [c.calibrated_p10, c.calibrated_p90],
          },
        ];
      }),
    );
    const drivers = (d.feature_importance?.["5"] ?? [])
      .slice(0, 3)
      .map((f: { feature: string; direction: string }) => ({
        feature: f.feature,
        direction: f.direction,
      }));

    return {
      cohort_size: d.cohort_size_actual,
      horizons,
      drivers,
      note: "Historical distribution of analogs, not a forecast.",
    };
  },
});

Version note: this is AI SDK v5 syntax (inputSchema + execute). On v4 the field is parameters; everything else is identical.

Step 3: Run the agent

Pass the tool to generateText and let the model call it. stopWhen: stepCountIs(n) lets the agent take the tool result and write its answer in the next step.

import { generateText, stepCountIs } from "ai";
import { openai } from "@ai-sdk/openai";
import { pullComps } from "./chart-library-tool";

const { text } = await generateText({
  model: openai("gpt-4o"),
  tools: { pullComps },
  stopWhen: stepCountIs(5),
  system:
    "For any stock setup, call pullComps and report the cohort distribution " +
    "(median, up-rate, and the calibrated 80% band) as historical fact. Never " +
    "give a single price target. If cohort_size < 30, say the sample is thin.",
  prompt: "What did setups like NVDA on 2025-03-03 (1d) do next?",
});

console.log(text);

That’s a grounded research agent. The model decides when to pull comps; you decide how the distribution is used.

Or connect the whole MCP surface

Prefer the full canonical toolset (search, pull_comps, cohort_introspect, cohort_members, and more) instead of hand-wrapping one endpoint? The AI SDK speaks MCP — point it at the hosted, keyless Streamable-HTTP endpoint and you get every tool at once:

import { experimental_createMCPClient as createMCPClient } from "ai";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const mcp = await createMCPClient({
  transport: new StreamableHTTPClientTransport(
    new URL("https://chartlibrary.io/mcp"),
  ),
});

const tools = await mcp.tools();          // the full canonical surface, keyless
// ...then: generateText({ model, tools, stopWhen: stepCountIs(5), prompt });
// await mcp.close() when you're done.

Same engine, same keyless access. Use the single tool() when you want a tight, controlled surface; use the MCP client when you want the agent to range across the whole toolset.

Why ground the agent this way

A language model asked “what happens after a setup like this?” will produce a confident, plausible, and unverifiable answer — we measured this: even a frontier model only stays calibrated by hedging to a ~2×-wider interval. Chart Library returns the actual cohort of analogs and their realized outcomes, with a calibrated band whose coverage is audited. The agent cites history instead of inventing it.

Frequently asked questions

Do I need an API key?
No. The core research loop — search, cohort_analyze / pull_comps, cohort_introspect — is free and keyless, up to 1,000 calls/day. Add a key only for higher volume or paid surfaces.
Does it work with Anthropic / other models, not just OpenAI?
Yes. The tool is model-agnostic — swap openai('gpt-4o') for anthropic('claude-...') or any AI SDK provider. The tool() definition doesn't change.
tool() vs the MCP client — which should I use?
Use the single tool() for a tight, controlled surface (one endpoint, your own trimmed result shape). Use experimental_createMCPClient against https://chartlibrary.io/mcp when you want the agent to range across the full canonical toolset. Both are keyless.
Does it forecast prices?
Never. It returns the historical distribution of what analogous setups did next — median, up-rate, calibrated p10/p90 — as a base rate, with a coverage receipt. It does not predict a single outcome.
Try it

Wire it into your TypeScript agent now.

The core loop is free and keyless — copy the tool, run it, no signup. Add a key only when you scale.

Related