Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/time-series-date-conversion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"@iqai/defillama-mcp": patch
---

Fix the classic Unix-seconds-vs-milliseconds JS Date trap in the time-series `exampleCall`s by demonstrating the `* 1000` conversion inline.

Session evidence from a recent Aiden Phase-3 Q3 run: the model fetched historical TVL, hit `new Date(p.date)` (without the `* 1000`), got `1970-01-XX` dates, then burned ~54s in retry/recover loops on the formatting and finally returned a degraded answer.

Root cause is upstream-shape, not model error: DefiLlama returns `date` as Unix **seconds** on the `/v2`-style endpoints, while JS `new Date(n)` expects **milliseconds**. Verified live (`curl https://api.llama.fi/v2/historicalChainTvl/Ethereum` → `date: 1782172800`): only `new Date(1782172800 * 1000)` resolves correctly to today; the bare form yields `1970-01-21`.

Same lever as the prior shape-discipline fixes (1.0.6 / 1.0.7 / 1.0.8) — the `exampleCall` is the contract surfaced to the model via `search_docs`. Three of the four time-series endpoints get the conversion demonstrated inline in their projection:

- `getHistoricalChainTvl`
- `getStablecoinCharts`
- `getStablecoinPrices`

Each becomes:

```js
date: new Date(p.date * 1000).toISOString()
/* p.date is Unix seconds; multiply by 1000 for JS Date */
```

The fourth time-series endpoint (`getHistoricalPoolData`) is handled differently because `HistoricalPoolItemSchema` declares `timestamp: z.string()` — i.e. already an ISO string from the yields API, no `* 1000` needed. Added an inline comment there making the asymmetry explicit so the model doesn't apply the `* 1000` pattern by mistake.

No instructions.md change. A generic "Unix-seconds vs ms" hint would be cheat-sheet territory; the right surface for this is the per-endpoint exampleCall demonstrating the right idiom.
8 changes: 4 additions & 4 deletions src/mcp/catalog/tool-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export const TOOL_METADATA: ToolMetadata[] = [
}),
responseSchema: HistoricalChainTvlSchema,
exampleCall:
"const {name} = await defillama.resolveChain(input); const series = await defillama.protocol.getHistoricalChainTvl({chain: name}); return series.slice(-90).map(p => ({date: p.date, tvl: p.tvl}))",
"const {name} = await defillama.resolveChain(input); const series = await defillama.protocol.getHistoricalChainTvl({chain: name}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, tvl: p.tvl}))",
},
// ── DEX (api.llama.fi) ─────────────────────────────────────────────────
{
Expand Down Expand Up @@ -418,7 +418,7 @@ export const TOOL_METADATA: ToolMetadata[] = [
}),
responseSchema: StablecoinChartsSchema,
exampleCall:
"const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); const series = await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id}); return series.slice(-90).map(p => ({date: p.date, totalCirculatingUSD: p.totalCirculatingUSD}))",
"const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); const series = await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, totalCirculatingUSD: p.totalCirculatingUSD}))",
},
{
name: "defillama_get_stablecoin_prices",
Expand All @@ -429,7 +429,7 @@ export const TOOL_METADATA: ToolMetadata[] = [
parameters: z.object({}),
responseSchema: StablecoinPricesSchema,
exampleCall:
"const series = await defillama.stablecoin.getStablecoinPrices(); return series.slice(-90).map(p => ({date: p.date, prices: p.prices}))",
"const series = await defillama.stablecoin.getStablecoinPrices(); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, prices: p.prices}))",
},
// ── Prices (coins.llama.fi) ────────────────────────────────────────────
{
Expand Down Expand Up @@ -635,7 +635,7 @@ export const TOOL_METADATA: ToolMetadata[] = [
}),
responseSchema: HistoricalPoolSchema,
exampleCall:
"const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => /* match on project/symbol/chain */)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp, apy: p.apy, tvlUsd: p.tvlUsd}))",
"const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => /* match on project/symbol/chain */)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp /* already an ISO string, unlike the date:number fields on /v2 endpoints */, apy: p.apy, tvlUsd: p.tvlUsd}))",
},
// ── Blockchain (coins.llama.fi) ────────────────────────────────────────
{
Expand Down
8 changes: 4 additions & 4 deletions src/mcp/search-docs/embedded-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const ENTRIES: IndexEntry[] = [
additionalProperties: false,
},
exampleCall:
"const {name} = await defillama.resolveChain(input); const series = await defillama.protocol.getHistoricalChainTvl({chain: name}); return series.slice(-90).map(p => ({date: p.date, tvl: p.tvl}))",
"const {name} = await defillama.resolveChain(input); const series = await defillama.protocol.getHistoricalChainTvl({chain: name}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, tvl: p.tvl}))",
},
{
kind: "method",
Expand Down Expand Up @@ -364,7 +364,7 @@ export const ENTRIES: IndexEntry[] = [
additionalProperties: false,
},
exampleCall:
"const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); const series = await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id}); return series.slice(-90).map(p => ({date: p.date, totalCirculatingUSD: p.totalCirculatingUSD}))",
"const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); const series = await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, totalCirculatingUSD: p.totalCirculatingUSD}))",
},
{
kind: "method",
Expand All @@ -379,7 +379,7 @@ export const ENTRIES: IndexEntry[] = [
additionalProperties: false,
},
exampleCall:
"const series = await defillama.stablecoin.getStablecoinPrices(); return series.slice(-90).map(p => ({date: p.date, prices: p.prices}))",
"const series = await defillama.stablecoin.getStablecoinPrices(); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, prices: p.prices}))",
},
{
kind: "method",
Expand Down Expand Up @@ -706,7 +706,7 @@ export const ENTRIES: IndexEntry[] = [
additionalProperties: false,
},
exampleCall:
"const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => /* match on project/symbol/chain */)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp, apy: p.apy, tvlUsd: p.tvlUsd}))",
"const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => /* match on project/symbol/chain */)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp /* already an ISO string, unlike the date:number fields on /v2 endpoints */, apy: p.apy, tvlUsd: p.tvlUsd}))",
},
{
kind: "method",
Expand Down
Loading