How do you map altFINS-style screener filters to AI agent trade calls? (2026)

Intro
A screener user picks a universe, stacks filters, sorts a list, and decides by eye. An agent cannot read a list by eye. The agent-side move is one scan_trade_calls request whose parameters encode the screen and whose rows carry a call, a confidence and a regime — a verdict per row, not a list. Every call the AlgoVault MCP returns is Merkle-anchored on Base L2, so a row an agent acts on stays checkable after the fact. For the named side-by-side with altFINS and Token Metrics, see our hub: The best altFINS alternative for AI crypto trading agents in 2026.
What changes when the screener user is an agent?
A screen is universe → filters → sort → a list a person reads. An agent run is parameters → one call → a verdict per row → act or stand aside. The filter stack becomes an arguments object; the sort becomes either a verdict-first row order or a client-side re-sort; the list becomes labelled rows an agent can branch on.
altFINS, in its own site's terms, offers coin screening, crypto charting, chart patterns, technical analysis, fundamental research, on-chain data and trade execution. Token Metrics appears here only as the query names it; this post states no facts about it. For how to choose between options for an agent, read the hub's section "How do you choose an altFINS alternative for an AI agent?" — this post never rebuilds its argument.
The point is the agent side: an agent never reads the list. It reads rows.
Implementation walkthrough: from screener filters to scan parameters
The AlgoVault MCP server exposes scan_trade_calls. Its live inputSchema is the map between a screener habit and an agent parameter. The table below pins each habit to a parameter, with values drawn verbatim from the live schema, and links each universe lens to the scan-lens post that owns its meaning.
| Screener habit | Parameter | Values from the live schema | Lens explained in |
|---|---|---|---|
| The biggest names by open interest | rankBy: "oi" + topN | topN 1–100, default 20 | Open interest and the liquidity floor |
| The most traded | rankBy: "volume" + topN | alias vol | Volume, gainers, losers, movers |
| Top gainers / losers / biggest movers | rankBy: "gainers" / "losers" / "movers" | aliases gain lose move | (same lens post as above) |
| Funding extremes | rankBy: "funding_positive" / "funding_negative" | aliases pfr nfr | Funding and volatility |
| The most volatile | rankBy: "volatility" | alias atr | (same lens post as above) |
| Open-interest change | rankBy: "oi_change" + oiChangeWindow + oiBasis | windows 1h 4h 24h; basis notional contracts | (same lens post as above) |
| A liquidity floor | minLiquidityUsd | number ≥ 0, optional | (same open-interest post as above) |
| Chart interval | timeframe | 1m 3m 5m 15m 30m 1h 2h 4h 8h 12h 1d, default 15m | — |
| Venue | exchange | enum of live perp venues, default BINANCE | — |
| Minimum strength | minConfidence | 0–100, non-HOLD only | — |
| Show neutral rows | includeHolds | boolean, default false | — |
| Rows returned | limit | 1–100, default 10 | — |
| A "why" column | includeReasoning | boolean, default false | — |
| Sort by the screen's own column | NO parameter — rankBy picks the universe, not the row order | rows arrive non-HOLD first, then confidence descending; re-sort client-side on rank_value (the default oi lens returns no rank_value) | — |
rankBy selects which topN perps get scanned. It does not re-order the returned rows. The row order is verdict-first (non-HOLD rows first, then confidence descending), and tied rows keep the lens order. Block 1 encodes a typical screen — the top Binance perps by 24h volume on 1h with a five-million-dollar liquidity floor — into one AlgoVault call.
// npm i @modelcontextprotocol/sdk@^1 (remote MCP endpoint, keyless) import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "screener-to-agent", version: "1.0.0" }); await client.connect(new StreamableHTTPClientTransport(new URL("https://api.algovault.com/mcp"))); // The screen: universe = the top Binance perps by 24h volume on 1h, liquid names only. // rankBy picks the universe, not the row order: rows come back verdict-first. const res = await client.callTool({ name: "scan_trade_calls", arguments: { rankBy: "volume", exchange: "BINANCE", timeframe: "1h", // lens meaning: the scan-lens series topN: 40, minLiquidityUsd: 5_000_000, // universe size + liquidity floor includeHolds: true, includeReasoning: true, limit: 10, // keep neutral rows; the "why" on non-HOLD rows } }); const scan = JSON.parse((res.content as { type: string; text: string }[])[0].text); Block 2 is a sample response captured when this post was prepared, kept solely as row-order evidence. The rows arrive in the scan's own order: non-HOLD rows first, then confidence descending. Row six here (ZEC) carries the largest rank_value of the ten, which is why this is not a volume order. Live rows and numbers will differ.
{ "scanned": 40, "eligible_non_hold": 0, "holds": 40, "errors": 0, "partial": false, "calls": [ { "coin": "UNI", "timeframe": "1h", "exchange": "BINANCE", "call": "HOLD", "confidence": 44, "regime": "TRENDING_DOWN", "rank_value": 229220831.696, "volume_24h": 229220831.696 }, { "coin": "LINK", "timeframe": "1h", "exchange": "BINANCE", "call": "HOLD", "confidence": 42, "regime": "TRENDING_DOWN", "rank_value": 190738085.175, "volume_24h": 190738085.175 }, { "coin": "ZEC", "timeframe": "1h", "exchange": "BINANCE", "call": "HOLD", "confidence": 37, "regime": "TRENDING_DOWN", "rank_value": 3339272866.48, "volume_24h": 3339272866.48 } ], "_receipts": { "verification_uri": "receipts.algovault — the per-call verify page keyed by call id", "disclaimer": "Informational analytics, not investment advice. Past performance does not guarantee future results." } } The AlgoVault schema sets additionalProperties: false, so every parameter an agent sends has to be one the server declared. That is also why the "which filters do not port" section exists: screeners expose some filters the agent simply cannot ask for, and the server refuses unknown keys rather than silently ignoring them.
How does the agent consume verdict rows instead of a screen list?
Three jobs: (1) tell a quota refusal apart from a result, (2) consume verdict rows in the scan's own order, (3) re-sort on rank_value only where the screen's column order matters. A quota refusal from AlgoVault arrives with error, code and message fields and no calls array — it is not an all-HOLD scan. An all-HOLD scan is a normal live state: the universe scanned, nothing cleared the verdict threshold, and the agent stands aside until the next candle.
type Row = { coin: string; exchange: string; timeframe: string; call: "BUY" | "SELL" | "HOLD"; confidence: number; regime: string; rank_value?: number }; if (!Array.isArray(scan.calls)) { // A quota refusal carries error / code / message and no calls: it is not an all-HOLD scan. console.log(scan.code, scan.message); } else { const rows = scan.calls as Row[]; // the scan's own order: non-HOLD first, then confidence const MIN_CONFIDENCE = 60; // your own bar, not a recommendation const actionable = rows.filter((r) => r.call !== "HOLD" && r.confidence >= MIN_CONFIDENCE); // Nothing cleared the bar: a quiet scan is a result, not an outage. Log it; rescan next candle. if (actionable.length === 0) console.log({ scanned: scan.scanned, eligible_non_hold: scan.eligible_non_hold }); // Your risk gates, sizing and execution live here; the scan returns the thesis only. for (const r of actionable) console.log(`${r.coin} ${r.exchange} ${r.timeframe}: ${r.call} @ ${r.confidence} (${r.regime})`); // The screen's own column, if you still want it: re-sort client-side (no rank_value under the default oi lens). const byLens = [...rows].sort((a, b) => (b.rank_value ?? 0) - (a.rank_value ?? 0)); console.log("by lens:", byLens.map((r) => r.coin).join(" ")); console.log("receipt:", scan._receipts.verification_uri); } The design trade-off here is worth naming. The agent could ignore _receipts entirely and treat the scan as a plain feed — faster to wire up, and a line less code. We log it anyway because the whole point of a Merkle-anchored verdict is that the agent can, later, prove which row it acted on. If you log only the row and drop the receipt, you keep the decision but lose the evidence; a month later there is no way to tell the auditor which anchored verdict drove the trade. Per-coin depth after the scan — the scan_trade_calls to get_trade_call loop — is covered separately.
Which screener filters have no scan parameter?
The live inputSchema declares no indicator threshold (an RSI level, a moving-average cross), no chart-pattern filter, no fundamental metric and no on-chain metric. additionalProperties: false means the server rejects any attempt to send one. That is deliberate: the AlgoVault composite verdict is itself the filter. Rebuilding an indicator stack on top of a composite verdict reintroduces the single-indicator failure mode the verdict exists to replace. The trade-off is real — you lose the "I want names where RSI is below the oversold line AND the fifty-day crossed the two-hundred-day" phrasing — but what you get back is a single column (call) an agent can branch on without re-implementing an indicator stack per language. Regime-based allow / deny gating (using the regime cell to filter rows after the scan) is a separate post.
Pitfalls when you port a screener into an agent
- Reading only
coinand dropping the verdict cells. That rebuilds the screen list and discards everything the verdict adds — thecall, theconfidence, theregime. - Expecting
minConfidenceto drop HOLD rows. The parameter applies to non-HOLD calls only — HOLD rows arrive whenincludeHoldsistrueregardless of their confidence number. If you want a HOLD-free list, dropincludeHoldsrather than raiseminConfidence. - Treating a quota refusal as an all-HOLD scan. A refusal carries
error,codeandmessageand nocalls; guard onArray.isArray(scan.calls)before any row logic. The two states look superficially similar (nothing actionable to trade) but the right response differs: back off and retry on a refusal, wait for the next candle on an all-HOLD. - Re-explaining a universe lens inside every agent. Each lens has one post that owns its meaning — link it, do not paraphrase it.
- Assuming
rankBysorts the returned rows. It selects the universe. The server returns rows verdict-first on purpose: an agent that needs to act wants the actionable rows at the top of the list, and only re-sorts onrank_valuewhen the screen's column order matters for display.
The agent still owns sizing, stops and execution. The scan returns the thesis.
FAQ
Which scan_trade_calls parameters replace the "universe" step of a screen?
rankBy (plus topN) picks the universe. The lens values are listed in the walkthrough table, each linked to the scan-lens post that owns its meaning.
Which parameters replace the "filters" step?
minLiquidityUsd, timeframe, exchange, minConfidence, includeHolds and limit. Add includeReasoning: true to get one-line reasoning on non-HOLD rows.
What replaces the "sort by column X" step?
No parameter. Rows arrive non-HOLD first, then confidence descending. Re-sort client-side on rank_value when the screen's column order still matters; the default oi lens returns no rank_value.
Which screener filters simply do not port?
Indicator thresholds, chart-pattern filters, fundamentals and on-chain metrics — the live inputSchema declares none of them. The composite verdict is the filter.
How does the agent tell a quota refusal from an all-HOLD scan?
A refusal has error, code, message and no calls array. An all-HOLD scan has calls populated with HOLD rows and eligible_non_hold at zero.
What's Next?
- Review the hub, The best altFINS alternative for AI crypto trading agents, and the three scan-lens posts linked in the walkthrough table for the lens that fits your screen.
- Background reading: the MCP tools spec and the JSON Schema reference.
⭐ Star the repo to follow new exchanges and signals: https://github.com/AlgoVaultLabs/crypto-quant-signal-mcp
