BattleGrid MCP Server
OfficialClick on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@BattleGrid MCP ServerList current market grid sessions"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
@battlegrid/mcp-server
MCP server for BattleGrid — play crypto prediction games, author trading strategies, and manage intelligence agents from AI agents.
It is a thin, authenticated stdio proxy to BattleGrid's remote MCP server (Stripe @stripe/mcp pattern — no business logic). It discovers tools, prompts, and resources live from the server and re-exposes them to local MCP clients (Claude Desktop, Claude Code, Cursor). Capabilities are always discovered live — this package never hardcodes the tool catalog.
v31 — the announced contract is read from the server, not declared here
The package version no longer tracks the server's contract, and no longer claims to. Through v30 it did: the proxy announced battlegrid@<package version> downstream, so the published number was read as the contract a client would reach, and keeping the two in step was a manual release chore. It did not hold — the package sat at 11.0.0 against a deployed contract of 19.3.0 for ten days, 5.1.0 was declared here and never published at all, and four of the last five releases were version bumps carrying no code change.
From v31 the proxy reads the contract version out of the upstream handshake at connect time and relays it verbatim. What a local client reads is therefore what the connected server just announced — correct by construction, on every connection, with no release involved.
Two versions, two meanings, and they are expected to differ:
What it describes | Where to read it | |
Package version | This proxy's own code — a fix here, a dependency bump, a docs correction |
|
Contract version | The server's wire contract, live | The stdio handshake ( |
Seeing package 31.x alongside handshake battlegrid@33.x — the package behind the contract — is the system working, not a missed release. That is the pair that looks alarming and is not: the contract moved, and no release here was needed. Both numbers are printed to stderr at startup, labelled.
What this changes for you: nothing about how you call anything. Upgrading the package no longer waits on a server deploy, and a server deploy no longer strands you on a package that names the wrong contract — reconnect and the announcement follows. Contract breaking-change notes are no longer keyed to package versions, since a contract move is no longer a release here; the v11-and-earlier notes below are kept as history, and the live vocabulary is always discovery.
Related MCP server: limitless-mcp
Contract history — v109 (a player's picks are entered through their entry draft)
Breaking at v109: submit_market_grid and update_market_grid are removed. v109 is the contract on
top of v108. A player's picks for a session live in their Market Grid entry draft — the same draft their
builder on the session page writes as they pick — and an entry is placed or changed by committing it.
Rejected input — something you send is no longer accepted
submit_market_gridandupdate_market_gridleave discovery with no alias. A call gets the standard unknown-tool refusal. Play is stage → get → commit:stage_market_grid_entry_draft({ request: { sessionId, draftVersion, axes } })with oneCELL_n: { cell: { coinId, prediction } }per grid position (CELL_0is the captain) and aRATIONALEcarrying the reasoning, confidence, model name and per-cell pick reasoning the removed tools required;get_market_grid_entry_draftfor the diff, diagnostics and impact; thencommit_market_grid_entry_draft({ request: { sessionId, draftVersion, expectedRevision } })on the player's word.random_submit_market_gridandsubmit_agent_gridare unchanged.
Added tools
get_market_grid_entry_draft,stage_market_grid_entry_draft,discard_market_grid_entry_draftandlist_market_grid_entry_drafts(mcp:read) read and stage the player's picks for free: no entry, fee, deposit or LLM access.commit_market_grid_entry_draft(mcp:wager) enters a session the player has not entered, charging the session's entry fee (the read'simpact.entryFee), or replaces a live entry's picks for no fee.
Refusal vocabulary
An entry that already exists is a revision
CONFLICTnaming the live entry's revision, withnextAct: get_draft: read the draft again and commit over the revision it returns.
Contract history — v108 (conditions are the only entry gate; signal rules are removed)
Breaking at v108: signal rules, the aggregate-score gate and the required-count gate are removed.
v108 is the contract on top of v107. A strategy's deterministic entry gate is its conditions: a
required condition, or a condition that carries a verdict. A strategy that declares neither never
trades, and its draft says so with the warning NO_ENTRY_CONDITION.
Removed tools
simulate_aggregate_score,list_strategy_signals,get_strategy_signal_definition,derive_strategy_rule_viewandget_coin_signal_previewleave discovery with no alias. A call gets the standard unknown-tool refusal.
Rejected input — something you send is no longer accepted
stage_strategy_draftrefuses theSIGNAL_RULESandSETUP_GATESaxes as unrecognized keys. The ATR floorminAtrPctis staged in theTRADE_LEVEL_POLICYaxis, beside the stop-loss multiples and the risk-reward minimum; aTRADE_LEVEL_POLICYstage without it is refused.decide_tradecitesentry.conditionChecklist: 1–8 items{ conditionKey, verdict, interpretation }over the decision's evaluated entry conditions, each key at most once,CONFIRMonly on a condition that read TRUE.signalChecklistandsignalModulesUsedare refused.The signal-log and agent-profile filters refuse
dominantBias.
Reshaped output — something you read has moved or been removed
Strategies, agents and drafts lose
signalRules,signalRuleTally,minAggregateScoreandminRequiredCount; the trading-config catalog loses the gate dial steps.Qualification and scans (
get_agent_coin_qualification,scan_agent_coins,scan_coin_agents) lose the score and required-count gates, the score shortfall and the signal readings. Scan rows rank qualifying first, then by ticker or agent name.Radar reads lose every score field: the activity events, the close decision, the curve samples (
{ at, atrPct, qualified }) and the curve digest.Entry decisions carry
conditionChecklistin place ofsignalChecklistandsignalModulesUsed. The public signal-log detail serves it as{ name, outcome }only.Signal logs and signal performance lose the aggregate score, bias, conflict and count fields; a signal-log detail loses its scorecard and serves
candidateLevels. The agent explorer loses its average score.
Narrowed enums
The qualification and screen reasons lose
AGGREGATE_BELOW_MINandREQUIRED_COUNT_BELOW_MINand gainNO_ENTRY_CONDITION, which is the first reason a strategy fails. The attempt reasons loseNO_ALLOCATED_SIGNALS, and the error codes loseREPAIR_REQUIRED.
A client that stages a signal rule or a gate, cites a signal in decide_trade, or reads a score field
breaks at v108, which is why this is a MAJOR.
Contract history — v107.1 (an arena entry is recorded before its fee moves)
Additive at v107.1: a paid entry can be recorded while its fee confirms. v107.1 is the contract on top of v107. The server now records your entry first, then sends the fee, then settles the entry on the venue's answer, so a fee can never move for an entry that was not recorded. No input changes.
Widened output
submit_market_grid,random_submit_market_gridandsubmit_agent_gridresults gainentryStatus:ENTERED, orPAYMENT_PENDINGwhen the entry is recorded and its fee is still confirming at the venue. A payment-pending entry plays once its fee lands before lock; otherwise it is released, and a fee that lands after that is refunded.entryStategainsPAYMENT_PENDINGonget_my_market_grid_entry.list_my_market_grid_entriesshares the element schema but never returns one.
Refusal vocabulary
details.wagerErrorCodegainsACCOUNT_NOT_UNIFIEDon the three submit tools: an entry fee is paid only from a unified-collateral account (unifiedAccountorportfolioMargin).
Behaviour behind unchanged schemas
check_market_grid_submissionanswershasSubmitted: truefor a payment-pending entry.update_market_gridrefuses a payment-pending entry withCONFLICTuntil it is in play.
Contract history — v106 (the Opening Bell: a session starts at its scored candle's open)
Breaking at v106: Two-Look entry is retired. A deployment slot drops entryStrategy, and a
deployment draft refuses one. v106 is the contract on top of v105.5. Every automated entry now looks
once, inside the Opening Bell window: from startsAt (the open of the one candle a session scores)
until lockAt (entries close).
Reshaped output — something you read has moved or been removed
entryStrategyleaves every deployment slot:get_deployment_policy,list_deployment_policies,get_deployment_policy_draft,pause_deployment_policy,delete_deployment_policy,preview_deployment_resolutionandtest_generate_deployment_grid.
Rejected input — something you send is no longer accepted
stage_deployment_policy_draftrefusesentryStrategyon aRULESrule and on theDEFAULT_SLOT, as an unrecognized key.preview_deployment_resolution'sSLOTSsubject andtest_generate_deployment_gridno longer publish the key, and they strip it if it is sent.
Widened output
Every Market Grid session surface gains
startsAt, the Opening Bell. That coversget_market_grid_session,list_market_grid_sessions,get_market_grid_results, and a deployment's target session and open entries. A Coin Grid summary'sstartsAtequals itslockAt.get_agent_prompt_context_preview's market context gainsstartsAtandmillisecondsSinceOpeningBell, both nullable.
Behaviour behind unchanged schemas
lockAtkeeps its name and now means "entries close", the end of the Opening Bell window, a fixed fraction of the session's duration afterstartsAt. ReadstartsAtfor when a session starts or what it scores. Submitting, editing and cancelling stay open untillockAt.SESSION_STARTrules matchstartsAt.
A client that reads entryStrategy from a slot, or stages one into a deployment draft, breaks at v106,
which is why this is a MAJOR.
Contract history — v103 (a column's contract carries its read size, measurement and series home)
Breaking at v103: a transform entry drops sideRequired and operandRequired, and
get_strategy_column_contract validates its ticker as a coin symbol. v103 is the contract on top of
v102.1.1, which changed only two deployment-draft input descriptions.
Reshaped output — something you read has moved or been removed
sideRequiredandoperandRequiredleave every transform entryget_metric_construction_hintsserves. Each restated a field the same entry already carries: readauthoring.parameters.side.requiredandauthoring.parameters.inputs.requiredinstead, where an absent parameter means the transform does not accept it.
Rejected input — something you send is no longer accepted
get_strategy_column_contract'sbenchmarkTickeris a coin symbol: trimmed, 1–20 characters of the coin-symbol charset. Any non-empty string was accepted before.
Widened input — something new you can send
The fixed
64maximums leave strategy authoring: a column'swindowandoffset, a custom section'scolumnsand theconditionsarrays. The configured budgetslist_strategy_vocabularyserves underbudgetsjudge them instead —sectionColumns,strategyConditions,conditionClausesandcolumnLookback.windowandoffsetkeep their integer floors.
Widened output
The column contract gains
lookback,measurementandseriesHome.lookback: { used, cap }— the bars the column reads, against the lookback budget a save holds it to.measurement: { transform, chainedTransform }— how each transform reads its series:readShape,windowUnit,priceBasisandnullPolicy.chainedTransformisnullfor an unchained column.seriesHome— where the metric's series is kept:candle,history, ornullwhen it has none, which is why a series transform does not compose on it.
list_strategy_vocabularygainsconditionGrammar: the group depth, the operator and group operator vocabularies and the verdicts a condition is written in.A category example gains
clause:{ op: 'between', low, high }, ornullfor an example with no recipe clause. The structure family gains two recipes —VWAP × distanceheldbetween -2 2("price within ±2% of VWAP") andSTRUCT_ZONES × nearestZoneDist(support)heldbetween 0 1.5("approaching support").A
windoworoffsetdeclaration gainsminimum, the smallest value the transform accepts.
Behaviour behind unchanged schemas
A column whose read size exceeds the lookback budget is refused with
REPORT_COLUMN_LOOKBACK_EXCEEDEDbyget_strategy_column_contractexactly as by a save, before the column is built. A window too large to build was a construction failure before.A value below a declared
minimumis refused naming the transform that declares it, including a chained transform's window.A spread with the wrong number of operands is refused naming the operand its operand order declares.
A
regimetimeframe reference resolves to the anchor's regime rung when the request names an anchor timeframe. It resolved tonullbefore.
A client that reads sideRequired or operandRequired, or sends a ticker outside the coin-symbol
charset to get_strategy_column_contract, breaks at v103 — hence the MAJOR.
Contract history — v102.1 (your agents' trading record)
Additive at v102.1: three read tools. v102.1 is the contract on top of v102. Nothing a client sends is refused and nothing it reads is reshaped.
Added tools
All three are mcp:read and read your own agents only.
list_agent_open_orders— the live orders (PENDING,ACKED,WORKING) of the positions your agents hold, newest first, each naming its agent.list_agent_trades— your agents' closed trades, newest close first, paged withnextCursorand narrowed bycoinTickerandagentId, with asummaryof the whole filtered scope: trade count, win count, win rate and net P&L in USD. Another user's agent reads as an empty scope.get_agents_portfolio_pnl— your agents' combined P&L over24H,7Dor30D, as 48 dated points ending on the period figure.
Contract history — v102 (your entry history's facets come with the first page)
Breaking at v102: list_my_market_grid_entries moves its counts, summary and agent options under one
facets object, sent with the first page only, and every entry gains a payout state. v102 is the
contract on top of v101.1.
Reshaped output — something you read has moved or been added
outcomeCounts,summaryandagentOptionsmove underfacets, unchanged inside it:facets.outcomeCounts,facets.summary,facets.agentOptions.facetscomes with the first page only. A page you read with acursorcarriesfacets: null. Keep the first page's facets for the whole scroll: every page of one read shares the same filters, so the first page's counts, summary and agent options describe all of them.Every entry gains
payoutState, inlist_my_market_grid_entriesandget_my_market_grid_entry:RECORDEDonce settlement has recorded both of the entry's payouts,UNDER_REVIEWif it left one unrecorded, andnullwhile the entry is in play or cancelled.facets.summarygainspayoutsUnderReviewCount: how many settled entries the money figures leave out because their payout is under review.
Behaviour behind unchanged schemas
An entry under review has
netPnl: null. It is listed underPLAYEDonly, never underPAIDorNOT_PAID, and the summary leaves both its fee and its payout out of every money figure, so the totals stay consistent. Today no entry is under review: the entries settled before settlement recorded both payouts now record0for the payout they never received, so their net reads the same as before.The five filters (
window,outcome,source,agentId,cursor) accept exactly what they accepted before. Only theagentIddescription changes: it now points atfacets.agentOptions.
A client that reads outcomeCounts, summary or agentOptions at the top level, expects them on every
page, or rejects unknown entry fields, breaks at v102 — hence the MAJOR.
Contract history — v101.1 (swing highs, swing lows and the structure label)
Additive at v101.1: the catalog gains three swing-structure metrics. v101.1 is the contract on top of v101; nothing you send today is refused and nothing you read is reshaped.
Widened input — something new you can send
SWING_FRACTAL_HIGHandSWING_FRACTAL_LOW— the most recent swing high and swing low: a bar whose high is at or above the two highs before it and strictly above the two after it (Pine'sta.pivothigh(2, 2)tie rule), and the mirror on the lows. A swing is published from the bar that completes its right side — never under its own bar, never from a forming bar — and held until the next swing on its side. They are candle price levels: every transform a candle level takes,levelInteractionincluded, and they rank across the board asdist_fractalHi/dist_fractalLo.SWING_FRACTAL_LABEL— the most recent swing against the previous one on its side:HH/LHfor a high (an equal high isLH),HL/LLfor a low (an equal low isHL),nonewhen no comparable pair is in reach. A held state, value-only; on too short a history it reads nothing.Break of structure is
dist_fractalHi crossesAbove 0/dist_fractalLo crossesBelow 0; a step of the level onto a newly confirmed swing never fires it.
Widened output
The catalog and vocabulary surfaces list the three keys, and the authoring digest's
metricKeysgains them.
Still refused
The headers are
fractalHi/fractalLo/fractalLabel. TheswingHi/swingLostems retired at v53 (the Donchian channel's old names) stay refused:dist_swingHiis stillCONDITION_COLUMN_UNKNOWN.
A client that treats the metric vocabulary as a closed set re-reads it; no request is refused — hence the MINOR.
Contract history — v101 (an agent on a radar cannot be archived)
Breaking at v101: archive_intelligence_agent refuses an agent held in any radar deployment, and its
refusal names a fourth blocker. v101 is the contract on top of v100.
Narrowed acceptance — the same request, refused where it was accepted
archive_intelligence_agentrefuses an agent that holds a slot in any radar deployment, paused ones included. The input schema is unchanged: a call that archived such an agent before now gets the existingCONFLICT. A paused radar still holds its slots, so pausing does not clear it. Find the coins withlist_radar_deployments, then commit a radar draft without the agent or calldelete_radar_deployment, and archive again.
Widened enum
details.archiveBlockers[].reasongainsRADAR_DEPLOYED, afterDEPLOYED,OPEN_TRADESandACTIVE_SESSION. Itsdetailreadsin N radar deployment(s), counting every radar deployment that holds the agent.
Behaviour behind unchanged schemas
An archived agent holds no deployment slot in either venue, so
activate_intelligence_agentarms nothing: the agent trades again only once you deploy it.A
commit_radar_deployment_draftorcommit_deployment_policy_draftthat races an archive of one of its agents is refused with the existingCONFLICT. Read the draft again; its diagnostics name the archived agent.
A client that reads archiveBlockers[].reason as a closed set, or expects an archive of a radar-deployed
agent to succeed, breaks at v101 — hence the MAJOR.
Contract history — v100 (what one bar did: level interaction, candle patterns, gaps, runs)
Breaking at v100: the bandTouch transform is retired and levelInteraction takes its place; a column
gains a buffer knob; the catalog gains twelve bar-geometry metrics. v100 is the contract on top of
v99.
Rejected input — something you send is no longer accepted
A column naming
bandTouchis refused by every tool that takes a column —stage_strategy_draft,preview_strategy_report,derive_strategy_rule_view,get_strategy_column_contractandget_metric_construction_hints— as any unknown transform id is. Where you want "where the close sits in the bands", readBB_TOUCH × value; where you want "what the bar's range did at a band", uselevelInteractionbelow.
Widened input — something new you can send
levelInteractionclassifies the newest bar of a column against a price level astouch_from_below,touch_from_above,reject_from_below,reject_from_aboveornone. The side is where the prior close stood against the prior bar's level. A reject — the range pierced the level and the close came back — reports reject, never touch, so "touched or rejected" is aninclause over both labels of a side. A close through the level readsnone: the break isdistance crossesAbove 0(orcrossesBelow 0) over the same level.It is offered on a level with a value at both bars it reads: a candle-homed level (a band,
VWAP, a moving average,ST_LINE,PSAR, the Ichimoku lines) or a prior-session level or pivot (PDH,PDL,PDO, the floor pivots,PRIOR_TPO_*). A prior-session level is a single read and takes no hold. It is not offered on a price fact (CLOSE,MARK,HLC3), a developing-session level (TPO_*), the naked points of control, or the Donchian rails.The header is
{code}_lvl—bbUpper_lvl.
buffer, a column knob declared bylevelInteractiononly: whole basis points of the level,0–500, default0. Only a touch reads it. A non-zero buffer names the header (bbUpper_lvl_b25), so two buffers are two columns.bufferon any other transform is refused as an undeclared parameter.Twelve metrics, all in the
pricefamily:candle patterns —
CANDLE_DOJI(boolean),CANDLE_PIN(lower_wick/upper_wick/none),CANDLE_ENGULFING(bullish/bearish/none),BAR_CONTAINMENT(inside/outside/none), each judged against the ten bars before it at TA-Lib's default settings;gaps —
GAPandGAP_RANGE_CLEAR(up/down/none) andGAP_PCT, the signed % from the prior close to the open;run lengths —
CLOSE_RUNandCANDLE_RUN, consecutive bars one way, capped at ±20;price sources —
HL2,HLC3,OHLC4, price facts that take every transform a close takes (HLC3 × aggregate(20)is the 20-bar SMA of typical price).
The patterns, gaps and runs compose
valueonly, and none of the twelve is ranked.
Reshaped output
effectiveParametersgainsbuffer— the declared default0or your value on alevelInteractioncolumn,nullwhere no transform in the chain declares it.A transform's authoring parameters may declare
buffer, and the transform and section column-role vocabularies readlevelInteractionwhere they readbandTouch.The authoring contract digest's
metricKeysgains the twelve keys.
A client that sends bandTouch, or reads effectiveParameters with a closed key set, breaks at v100 —
hence the MAJOR.
Contract history — v99 (your arena entries are one owner-only history)
Breaking at v99: four tools are retired, two are added, and get_agent_journal loses a field. v99 is
the contract on top of v98.
Every Market Grid entry you made — by your deployed agent, an agent you asked for, an MCP client, Commander, a grid you built or dice you rolled — is now read from one history, and only by you.
Removed tools
get_agent_game_history,get_user_agent_game_history,get_public_agent_game_historyandget_mcp_reasoning_journalare gone, with no alias; a call gets an unknown-tool error. The replacements:get_agent_game_history { agentId }→list_my_market_grid_entries { agentId };get_user_agent_game_history→list_my_market_grid_entrieswith noagentId;get_public_agent_game_history→ none: arena entry history is owner-only, so no public read replaces it;get_mcp_reasoning_journal { sessionId }→get_my_market_grid_entry { sessionId }, whosereasoningis the reasoning recorded with the entry's picks.
Added tools
list_my_market_grid_entries { window?, outcome?, source?, agentId?, cursor?, limit? }(mcp:read) pages every entry you made, newest first by its first submission.outcomeisPLAYED(the default: every entry except a cancelled one),IN_PLAY,PAID(a payout above zero),NOT_PAIDorCANCELLED;sourceandagentIdfilter by the entry's original author. The same read returns one count per outcome, a summary (paid rate, totals, net, averages) and the agents you can filter by. Page withnextCursor.get_my_market_grid_entry { sessionId }(mcp:read) returns your entry in one session — the same record a list row carries. A session you never entered isNOT_FOUND.Each entry carries its
entryState(WON,PLACED,NO_WIN,VOID_NO_SUBMISSION,IN_PLAYorCANCELLED), itsentryFeeand signednetPnl(null while in play or cancelled), a cancelled entry'srefundState, itsoriginalSourceand originalagent, andedited— true once another author has rewritten the picks.reasoningis null on an edited entry, because the record no longer describes the picks. Neither tool accepts a user identity: both read the authenticated account's own entries.
Narrowed output
get_agent_journalno longer returnsrecentGames. An agent's arena entries are read withlist_my_market_grid_entries { agentId }.
A client that calls a retired tool, or reads recentGames, breaks at v99 — hence the MAJOR.
Contract history — v98 (a clause can read the bar before)
Breaking at v98: a condition clause accepts six operators that compare a column with its own previous completed bar, and a clause's evidence gains a second shape. v98 is the contract on top of v97.
Widened input — something new you can send
A condition clause on
stage_strategy_draftandpreview_strategy_reportaccepts:crossesAbove/crossesBelowwith a numbervalue— TRUE when the value is above (below) it on the decided bar and was at or below (at or above) it on the bar before. A bar landing exactly on the level has not crossed.increased/decreasedwith no literal — strictly above (below) the prior bar's reading. Avaluebeside them is refused.enters/exitswith onelabel— the label read now and not on the bar before, or the reverse.
Numeric headers take the crossings and changes; classification, direction and boolean headers take the transitions; event and rank headers take none of them. A strategy's served
conditionOperators(preview_strategy_reportatdetail: "detailed", and the scalar vocabulary) lists exactly what each header admits on that strategy;get_strategy_column_contract, which reads no strategy's stored history, lists the operators the header's kind takes.Each of the six reads two completed bars, never the forming one, on every basis, so its reading is never provisional. A failed read on either bar is
UNRESOLVED; a bar with no value readsFALSE.
Narrowed acceptance — the same request, refused where it cannot be read
A header whose stored history keeps only one bar (
conditionHold.maxWindow1) admits none of the six: the clause is refused withCONDITION_OPERATOR_UNSUPPORTED, and every operator refusal'sallowedDomainnow lists the operators that header admits.crossesAbove,crossesBelow,entersandexitsare edges: a hold withatLeastabove 1 is refused (CONDITION_HOLD_ILLEGAL) — hold them "within n closes". A prior-bar clause's largest window is one bar less than its header's, and the frame-read budget counts the bar before.
Reshaped output — a clause's evidence has two shapes
A prior-bar clause's evidence carries
priorOperand— the same column's reading on the bar before — and adecidedBarthat is never null. It appears onpreview_strategy_report,get_radar_close_decisions,list_gate_blocks,preview_radar_resolution,get_signal_log,get_agent_coin_qualification(a condition level's clauses) and the trade register an agent reads.A current-bar clause's evidence is unchanged and carries no
priorOperand; every record written before v98 reads as it was written.Rendered evidence lines read prior before current:
RSI14_now crosses above 50 (49.6 → 50.4).
Widened output
A condition series'
BOUNDthresholdopwidens tocrossesAbove/crossesBelow, drawn at the level crossed. A change or a transition draws no threshold.The authoring contract digest gains
domains.conditionOperator, the thirteen operator names.
A clause reader that assumed one evidence shape, or an operator vocabulary of seven, breaks on the same call at v98 — hence the MAJOR.
Contract history — v97 (a signal records the evidence it decided on)
Breaking at v97: an evaluated signal's evidence keys move, BB_TOUCH touches by %B, and a crossing is
read on the bar that leaves zero. v97 is the contract on top of v96.
Changed meaning — the same record, different keys
An evaluated signal's
indicatorValues— onget_signal_log,get_public_agent_signal_log_detailandget_coin_signal_preview— records the last price a live-priced signal evaluated underlast_price. Six signals recorded it underprice:MA_SMA200_ABOVE/_BELOW,SR_AT_SUPPORT/_RESISTANCEandBOLLINGER_LOWER_TOUCH/_UPPER_TOUCH; the three structure-approach signals now record it too.pricemeans the last closed close on every signal that records it.The Bollinger touches add
bb_upperandbb_lower, and theirbb_percent_bis the live %B they compared — never the closed-bar %B.The EMA 5/13 crosses add
ema_cross(1/−1), and the sector comparison signals addema5,ema13andema20.MFI_SUSTAINED_*andCVD_BULLISH/CVD_BEARISHrecord their prior-bar value when there is one, and omit it when there is not.
A signal records exactly the keys its declared vocabulary names. Records written before v97 keep the keys they were written with, so a reader of history meets both.
Changed values — the same fields, one rule each
Crossings. One zero-cross rule decides every crossing: a value that reaches zero has not crossed; one that leaves zero has. Every
crossDetectcolumn (MACD_cross,PPO_cross,ROC_crossand authored ones), theEMA_CROSSlabel, the MACD / PPO / stochastic cross signals andFUNDING_RATE_FLIPPINGread no crossing on a bar that lands exactly on zero. They read the crossing on the bar that leaves it.BB_TOUCHandbandTouchclassify a touch when %B is strictly below0.05or above0.95— the touch signals' default thresholds. Before, a price within 2% of the band width of an edge, inclusive, touched. A degenerate band readsnone, and aBBtouchcondition now agrees with the scorecard's touch at defaults.RSI zone. The projected RSI state is overbought strictly above 70 and oversold strictly below 30, as the
RSI14_zonecolumns already read it.Descriptions. A served rule description states the thresholds that rule evaluates, and the ROC descriptions read "positive and rising".
Widened output
get_strategy_signal_definition'sindicators[].metagainsoptional: trueon a key the signal records only when its read is present —prev_cvd_valueonCVD_BULLISH/CVD_BEARISH,prev_mfi14_valueonMFI_SUSTAINED_*. Every other key is always recorded when the signal has the data to evaluate.
A key read at v96 is absent at v97 on the same signal, and a touch or a crossing read at v96 can read differently on the same bar — hence the MAJOR.
Contract history — v96 (a metric's meaning is declared once)
Breaking at v96: efficiency admits only a level or a price-fact series, and nearestZoneDist changes
sign. v96 is the contract on top of v95.
Rejected input — something you send is no longer accepted
efficiencyover an increment or an oscillator metric —CLOSE_CHANGE,VOLUME,RSI14,FUNDING_RATEand the like — is refused at construction, and so isefficiencyas the outer ofdistanceorspread(VALIDATION_ERROR, authoring codeREPORT_COLUMN_CONSTRUCTION_FAILEDorREPORT_COLUMN_CHAIN_UNSUPPORTED), by every tool that builds a report column.distanceandspreadchaintrajectory,aggregateandrank; the column contract and the vocabulary tools serve exactly those sets.BB_PCT_Bno longer composesmaxShare:%Bleaves[0, 1]when price closes outside a band.A
rankcolumn with noorderingis gated as the defaulthiit ranks by, so it is refused wherever a writtenhiis (REPORT_COLUMN_PAIR_UNSUPPORTED). Write the ordering you mean.transformIdis the closed enum of the seventeen transforms andchainedTransformIdthe enum of the five chain outers, where each was any 1–40 character string. An id outside them is refused by schema.
Changed values — the same fields, a declared meaning
nearestZoneDistis price's distance from the zone midpoint,((price − mid) / mid) × 100: positive when price is above the zone, negative below it. It was the midpoint's distance from price, with the opposite sign. A range symmetric about zero reads the same window; a one-sided bound needs its sign flipped.BB_PCT_Boffers all four rank orderings.A
classifyZonecolumn declares and resolves its own metric's vocabulary in canonical labels — ADX's strength bands, MFI's five bands — not a sharedoverbought / oversold / neutralset.highDev,lowDevandoiRegimeread live sources and declare the live clock, so a condition over them is provisional on the live frame.A chained column's formula binds its outer half to the inner column's header (
dist_VWAP), and every relative-change formula reads((value − reference) / reference) × 100.
Reshaped output
An authoring parameter's
defaultValueonget_metric_construction_hintsis typed per knob —windowandoffsetintegers,orderinga rank ordering — and absent onside,inputsandbars.A rendered section column's
roleis one enum of the same members, where it was a union of an enum and five literals.
An input v95 accepted is refused at v96, and a value read at v95 reads with the opposite sign — hence the MAJOR.
Contract history — v95 (a refusal names the verdict it applied)
Breaking at v95: a gate block's conditionVerdict is the verdict applied, not the one read. v95 is the
contract on top of v94.
Changed meaning — the same key, a different question
On
list_gate_blocks, a conditions-stagereasonDetail.conditionVerdictis the verdict applied — the one bound for the pair, which can be a held verdict this evaluation did not read — where it was the verdict the conditions read. It is served only on aCONDITION_VERDICT_OPPOSES_DIRECTIONorCONDITION_EXIT_RULE_CLOSES_DIRECTIONblock recorded since v95; aREQUIRED_CONDITION_FALSEblock, which no verdict decided, and a block recorded before v95 no longer carry it. ReadobservedVerdictfor what the conditions read.
Widened output
The reason detail gains
observedVerdict, served on every conditions-stage block, andheldVerdict,verdictDwellResolutionsandverdictDwellRequired— the dwell state the binding was resolved from, served besideconditionVerdictand absent where it is.get_signal_logandget_public_agent_signal_log_detailembed the same schema and never fill these keys: a block writes no signal log.get_signal_log'sconditionEvaluation.verdictandget_agent_journal'smarketSnapshot.conditionVerdictkeep meaning the verdict the evaluation observed.
A key read at v94 answers a different question at v95 — hence the MAJOR.
Contract history — v94 (every value you read names the bar it came from)
Breaking at v94: a timeless metric refuses an offset, and a value with no reading on its bar reads absent
instead of an older bar's. v94 is the contract on top of v93. A column reads the bar it names, or nothing.
Rejected input — something you send is no longer accepted
A
valuecolumn over a timeless metric — oneget_strategy_column_contractserves withtimeframeMode"timeless": funding, the regime, the previous-session levels and the other bundle reads — withoffsetabove0is refused at construction (VALIDATION_ERROR, authoring codeREPORT_COLUMN_CONSTRUCTION_FAILED) by every tool that builds a report column, the strategy and agent draft tools andpreview_strategy_reportamong them. It used to be accepted and ignored. Drop the offset: a timeless metric has no earlier bar.
Changed values — the same fields, read at a named bar
A trajectory, zone, state or cross value whose current bar holds no reading is now absent, instead of the newest older bar's value. A trajectory's slots keep a missing bar as an empty slot, in place.
A window — a trajectory's,
efficiency's,maxShare's — counts the newest bars, present or not.A candle label column at
offset Nreads bar t−N's label; it used to read the latest.On a decided bar, a condition over a published change (
chg5m…chg24h) oroiRegimereads unresolved, and a condition over a higher-timeframe column reads unresolved during that bar's close lag.
Behaviour behind unchanged schemas
An Arena deployment that routes on the regime reads
WARMINGwhile its regime bar is landing — the regime for the bar the clock just closed is not published yet — and plays nothing, its default slot included. Generating, submitting or updating a grid for an agent on such a deployment is refused withSERVICE_UNAVAILABLEin those seconds; retry.
Something read at v93 reads absent at v94, and an input v93 accepted is refused — hence the MAJOR.
Contract history — v93 (a trajectory reads at least two observations)
Breaking at v93: a trajectory column below two observations is refused. v93 is the contract on top of v92. Nothing in a schema moves.
Rejected input — something you send is no longer accepted
A custom column
{ transformId: "trajectory", window: 1 }, plain or as the chained outer ofdistance × trajectory, for any metric:stage_strategy_draftreportsREPORT_COLUMN_CONSTRUCTION_FAILEDin its diagnostics, naming the minimum,commit_strategy_draftrefuses it, andpreview_strategy_reportrefuses a report carrying one with the same error. A window of one has no build-up, and its trend compared a reading with itself, so it readflatfor every coin on every bar. The trajectory's servedwindowdescription states the minimum.
Changed values
The CVD section renders its buy-pressure, buy-trade and sell-trade columns as window-4 trajectories —
_t3,_t2,_t1,_now,_trendeach — and every_nowkeeps its value. Their_trendcells read a direction where they readflat, and the CVD-crowd convergence classifies from that buy-pressure column, socaptainConfcan readtrueandconfcan readhigh.
Something accepted at v92 is refused at v93 — hence the MAJOR.
Contract history — v92 (every section you read is the JSON the agent reads)
Breaking at v92: the section fields are reshaped, the report preview's default narrows, and its size is measured
in characters. v92 is the contract on top of v91. Every tool that published a section as markdown text publishes
it as the JSON object the agent reads, and preview_strategy_report takes a detail level.
Reshaped output — a section is its JSON
A section read is { shape: "table", section } — { id, title, notes, provenance?, anchorTf, groups, columns, rows, addendum? }, rows positional and every cell its raw value, each column stating its unit, precision, bar
set (bars) and, on a rank column, the ranked set's universe, with a key absent where the column has nothing to
say — or { shape: "facts", section }, { id, title, facts, glossary }. It replaces:
preview_strategy_report.renderedSections[]:{ sectionKey, section: { title, text }, structure, authoredNote }→{ sectionKey }and a section read, or{ sectionKey, shape: "skipped" }for a row that rendered nothing. A custom section's authored note is its section'snotes.get_market_context.sections[]:{ kind, title, content, structure }→{ kind }and a section read.get_context_source_full_preview.content→read, a section read ornull.get_context_sources_preview: each source'svalue→section, a section read ornull, a coin-row table sampled to its first five coins.get_signal_log.reportSections[]: a section read,skipped, or{ sectionKey, shape: "text", title, text }— a row stored before custom sections carried a structure, kept as its agent read it.preview_strategy_report.conditionsTableTextand.tradeConditionsBlockText→gridConditionsandtradeConditions, each{ glossary, register }: the glossary prose and the register object the text carried as one JSON line. Each isnullwhere its text was.
Widened output — the register names what decided the verdict
decidedByjoins the conditions register: on eachGRIDcoin and on theTRADEregister, the key of the condition theverdictcame from,nullwhen no single condition made the call. It reaches you ingridConditions.register,tradeConditions.register, and the registerget_agent_prompt_context_previewshows, and the conditions glossary gains the sentence defining it.
Narrowed default, new input — preview_strategy_report detail
detailis"concise"(the default) or"detailed". A concise preview is exactly what the agent reads.conditionColumnsandconditionOutcomesleave the top level. With each row'sheaderBindings({ sectionKey, columnIndexByHeader }) they make upauthoring, served only at"detailed"andnullotherwise. If you read either field, ask for"detailed"and read it underauthoring.marketReadMarkers[]loseresolvedNameandresolvedValue.
Result size in characters
tokenCountModelandbudgetUsage.estimatedTokensleave the preview.budgetUsage.resultCharsis the exactJSON.stringifylength of the response that carries it, its own digits included, against the cap.list_strategy_vocabularydropsbudgets.estimatedTokens, andpreviewExecutionLimits.maxResultBytesis renamedmaxResultChars.A preview over the cap is refused with
PREVIEW_LIMIT_EXCEEDEDandlimit: "result_chars";estimated_tokensandmcp_result_bytesare retired. Every preview refusal now carries itspreviewErrorCodeand its bound (deadlineMs, orlimit,actualandmaximum) in the error's details.preview_strategy_report,get_market_contextandget_context_sources_previewdeclare the cap as_meta["anthropic/maxResultSizeChars"], so Claude Code keeps a large result inline.
Something read at v91 is reshaped or gone at v92, and a default narrows — hence the MAJOR.
Contract history — v91 (every data block an agent reads is one JSON object)
Breaking at v91: one output field is removed, and the published text that is the agent's prompt carries JSON.
v91 is the contract on top of v90. Every data block BattleGrid's trading agents read — the strategy's resolved
conditions, fact sections, and the trade-decision blocks — is one JSON object naming itself with id and title.
Instructions stay prose under their ## Title. The tools that show you the prompt therefore show that JSON.
Removed output — something you read is gone
promptDataSerializationleavesget_signal_log's owner detail. There is one serialization, so a trade record stores none.reportSectionsandmissingDataare unchanged, and a trade recorded before capture still serves both asnull.
Reshaped output — the same fields, carrying JSON
preview_strategy_report.conditionsTableTextis the conditions glossary as a## Strategy Conditions Glossaryinstruction, then the conditions register as one JSON line:surface: "GRID", andcoins[]each withoutcomeskeyed by condition andverdictby display name (nullwhen the strategy declares no verdict). It was a markdown table followed by the glossary.preview_strategy_report.tradeConditionsBlockTextis the trade glossary, a blank line, then the target coin's register as one JSON line:surface: "TRADE",verdictas{ name, description }, andconditions[]each withoutcome,required,provisional,counts,exitRuleand the evaluator'sevidence. It was a## STRATEGY CONDITIONSheading, a table and evidence bullets.A fact section's text is its JSON,
{ id, title, facts, glossary }withfactskeyed by label, inget_market_context.sections[].content,get_context_source_full_preview.content,preview_strategy_report.renderedSections[].section.text, and theget_signal_logreport of trades recorded from v91 on. The session field is a fact section. A table section's text is still its markdown.get_agent_prompt_context_preview.promptSections[].contentis each section as the agent reads it: a data section's JSON, a table's included, and an instruction's prose.
Widened enum
get_agent_prompt_context_preview.promptSections[].kindgainsstrategy-conditions-glossary, the instruction read before the conditions register. The market-context reads exclude it, as they exclude the register.
Something read at v90 is gone or reshaped at v91 — hence the MAJOR.
Contract history — v90 (Arena and radar deployments join the draft lifecycle)
Breaking at v90: input is refused and reshaped, output is reshaped, five error codes are removed. v90 is the contract on top of v89. The Arena and radar deployment drafts now run the same five-tool lifecycle as agents and strategies, and the preview token is retired: a deployment commits on the draft version and the revision you read.
Rejected input — something you send is no longer accepted
previewTokenandconfirmleavecommit_deployment_policy_draft,commit_radar_deployment_draft,resume_deployment_policyandresume_radar_deployment.confirmleavesdelete_deployment_policy,delete_radar_deployment,discard_deployment_policy_draftanddiscard_radar_deployment_draft.Every input is strict, so a call naming either key is refused as an unrecognised key.
Reshaped input
The commits take
{ request: { presetId | coinId, draftVersion, expectedRevision } }: exactly thedraftVersionandcommittedRevisionthe kind'sget_*_draftreturned (expectedRevision: nullfor a first deployment).The resumes take
{ presetId | coinId, expectedPolicyId, expectedRevision }: thepolicyIdandrevisionaCOMMITTEDpreview returned. Any other deployment or revision is aCONFLICT, and nothing is armed.delete_radar_deploymentgainsexpectedPolicyIdbesideexpectedRevision, asdelete_deployment_policyalready took. A delete naming a policy since removed and redeployed matches nothing.The discards take
{ request: { presetId | coinId, draftVersion } }with no dry run. A draft that moved, or none, isDRAFT_VERSION_MOVED.
Reshaped output — something you read has a new shape
stage_deployment_policy_draftandstage_radar_deployment_draftgaindiagnostics.get_deployment_policy_draftandget_radar_deployment_draftanswer the lifecycle read{ draft, draftVersion, committedRevision, diff, diagnostics, impact }, where they answered{ draft }.impactis the resolution the commit would arm and whether it plays (enabledAfterCommit), plus a radar first deployment's coin-cap reading (admission); it is null when a diagnostic refuses.The commits answer
{ presetId | coinId, revision, draftVersion }. The radar commit answered{ revision }.The discards answer
discarded: true, neverfalse.preview_deployment_resolutionandpreview_radar_resolutionlosepreviewTokenand gainpolicyIdandrevision: the deployment the preview resolved over, null forSLOTSand a first deployment.
Removed error codes
TOKEN_EXPIRED, TOKEN_BINDING_MISMATCH, INVALID_TOKEN_SIGNATURE, INVALID_TOKEN_FORMAT and INVALID_TOKEN_CLAIMS
leave the error vocabulary and every draft diagnostic's code enum — so the stage and get outputs of the agent and
strategy drafts move too.
Behaviour behind the new schemas
A retried commit replays its receipt. Both commits are destructive and idempotent on
<presetId | coinId>:<draftVersion>, as the agent and strategy commits are.Refusals name the next act. A moved draft or revision carries
details.nextAct: "get_draft"; a refusal the draft can repair (INVALID_DEPLOYMENT_POLICY, or the radar'sVALIDATION_ERROR) carries"stage"; a retired arena carries none and names the remedy.A revision conflict names the revision that won in
details.actualRevision, null when no deployment remains.A
DRAFTpreview at a version the draft is not at isDRAFT_VERSION_MOVED.A draft whose arena or coin can no longer be deployed is still served, with the refusal in its diagnostics and a null impact, so you can show it and discard it.
A radar resume of a policy already trading answers its revision and changes nothing.
The radar per-user coin cap is
FORBIDDEN, with nonextAct, where it wasVALIDATION_ERROR.
Something accepted at v89 is refused at v90 — hence the MAJOR.
Contract history — v89 (a new strategy is held to today's operator bounds)
Breaking at v89: acceptance narrows behind unchanged schemas. v89 is the contract on top of v88.1.
Narrowed acceptance — commit_strategy_draft, preview_strategy_report (DRAFT source)
A create judges every dial of the new strategy at the operator bounds configured now. That includes a dial the author typed, one a fork copied from its source, and one left at the creation default. The operator bounds are the platform's ATR %, stop-loss ATR-multiple and risk:reward limits.
At v88.1 a create judged only the dials that differed from the fork's source or the creation default. So a copied dial outside a bound tightened since the source stored it was committed.
At v89
commit_strategy_draft(expectedRevision: null) refuses that create withVALIDATION_ERROR, naming the dial and the bound, for exampleminAtrPct (0.1) must be >= 0.15. Nothing is created and the draft is unchanged: stage the dial inside the bound and commit again.preview_strategy_reportwithsource: { kind: "DRAFT", … }composes the draft exactly as the commit does, so it refuses the same draft with the same error where v88.1 rendered it.stage_strategy_draftandget_strategy_draftreport the error in their diagnostics before the commit.fork_strategy's description now says this, so its description hash moves.
Behaviour behind unchanged schemas
An edit still judges only what it moves. An update or a restore carries a dial it leaves unchanged, so a stored value outside a since-tightened bound stays readable and saveable on its own strategy.
The platform keeps its defaults creatable. An operator cannot set a bound that would put a creation default outside it, so a create that leaves a dial untouched is never refused for a value the platform seeded.
Something accepted at v88.1 is refused at v89 — hence the MAJOR.
Contract history — v88.1 (an open position keeps the exit rules it opened under)
Additive at v88.1: acceptance widens and one served value narrows; no input or output schema changes. v88.1 is the contract on top of v88.
Widened acceptance — commit_strategy_draft, restore_strategy, commit_agent_draft
An open position no longer refuses a strategy commit, a restore or a rebind. At v88 a write that would leave an exit rule unreadable on a coin a bound agent held an open position on was refused with
CONDITION_UNREADABLE_BY_RADAR_SCANorCONDITION_OPERAND_UNSERVED_IN_LANE. At v88.1 it commits. A radar slot's coin and a pending manual request's coin are still checked, and refused exactly as before.The three tools' descriptions now say this, so their description hashes move.
Narrowed served value — the readability refusal's coinRole
The refusal's error context serves
coinRoleasSLOTorMANUAL_REQUESTonly.OPEN_POSITIONis no longer emitted. A client switching on it loses a member it can no longer receive.
Behaviour behind unchanged schemas
An open position keeps what it opened with. An agent position records the exit rules it opened under, with the report and timeframes they read, and its exit rules are evaluated from that record for the life of the trade. An exit-rule, exit-policy or level edit reaches only positions opened after it; a revision, rebind, SYSTEM update or timeframe change after the open never reaches an open trade.
Nothing accepted at v88 is refused at v88.1.
Contract history — v88 (an exit rule names the side it closes)
Breaking at v88: a condition body accepted at v87 is refused, and condition and chart output is reshaped. v88 is the contract on top of v87.
A condition is an entry condition or an exit rule, never both. An exit rule names the side it closes,
where exit: true read that side from the condition's verdict.
Rejected input — stage_strategy_draft, preview_strategy_report
A condition no longer carries
exit. A condition entry naming it is refused with the unknown-key error, on theCONDITIONSaxis ofstage_strategy_draftand in the conditions ofpreview_strategy_report'ssource: { kind: 'FIELDS', … }.Every condition requires
exitSide:LONG,SHORTorBOTHfor an exit rule,nullfor an entry condition. It is nullable, never optional; a condition without it is refused.An exit rule carries
verdict: nullandrequired: false, and an entry condition cannot reference an exit rule. The builder refuses each with a typed remediation error —CONDITION_EXIT_RULE_CARRIES_VERDICT,CONDITION_EXIT_RULE_REQUIRED,CONDITION_ENTRY_REFERENCES_EXIT_RULE— in a staged draft'sdiagnostics, and as the refusal of itscommit_strategy_draft. An exit rule may reference anything: "exit when the entry setup stops holding" is an exit rule whose definition isNOTaconditionRefto the entry condition.
Reshaped output
Every condition a strategy read or commit returns carries
exitSideand noexit:get_strategy,archive_strategy,restore_strategyandcommit_strategy_draft.get_agent_coin_qualification's chart names the side each exit rule closes. Every served level gainsreferrers,{ conditionKey, conditionName, exitSide }for each of its set's declared conditions that reach it. Every series clause referrer and everyconditionsWithoutLevelsentry gainsexitSide,nullon every entry-set referrer. TheENTRYset no longer lists exit rules; they are listed underEXIT.
Widened enums
CONDITION_EXIT_RULE_CLOSES_DIRECTIONjoins the qualification gate code, the trade evaluation attempt reason, the trade gate reason and the trade screen reason. It can appear in the output ofget_agent_coin_qualification,scan_agent_coins,scan_coin_agents,list_gate_blocks,get_signal_log,get_public_agent_signal_log_detail,get_radar_activity,get_radar_activity_summary,get_radar_close_decisions,get_radar_deployment,list_radar_deployments,preview_radar_resolution,propose_entry_decision,get_trade_conversation,get_agent_budgetandreset_agent_drawdown_baseline. A gate block under it names the refusing exit rule indecidedBy.BLOCKED_BY_CONDITION_EXIT_RULEjoins the radar evaluation outcome, onget_radar_activityandget_radar_activity_summary.
A client switching exhaustively on any of them meets a member it has not seen.
Behaviour
An exit rule closes exactly the side it names. A settled TRUE on a completed bar closes the agent's open position on that side. No verdict chooses it;
exit: trueclosed the side its verdict opposed, and both sides for aNEITHERornullverdict.While an exit rule reads TRUE, its side is not offered for entry. Scan qualification and compose refuse that side under
CONDITION_EXIT_RULE_CLOSES_DIRECTION, after any verdict refusal of the same side.A stored
exit: truecondition is split and closes what it closed. It keeps its verdict as an entry condition, and its strategy gains an exit ruleCLOSE_ON_<conditionKey>, aconditionRefto it closing the side its verdict opposed (UP→SHORT,DOWN→LONG,NEITHER→BOTH). A strategy read returns both.Unchanged names keep their place.
list_strategies'conditionTally.exitnow counts exit rules, and a decision record's conditiondeclarationkeepsexit: boolean, true for an exit rule.
A strict client that rejects unknown keys fails to parse every condition and every chart level at v88; list the tools again after the server deploys.
Contract history — v87 (a fork's create resolves its source again)
Breaking at v87: a fork's create can now be refused where it committed. v87 is the contract on top of v86. No schema, description or tool changes.
Narrowed acceptance
A create draft that fork_strategy opened is committed by commit_strategy_draft with
expectedRevision: null, or by the builder's Create. That create now resolves the strategy it copied
again. It uses only the source id and revision in the draft's ORIGIN, by the same rule the fork applied:
A source that moved past the forked revision refuses the create with
CONFLICTand nodetails.nextAct. Reading the draft again cannot cure it; the message says to discard the draft and fork again.get_strategy_draftandstage_strategy_draftreport the same refusal in the draft's diagnostics.A source you can no longer see — archived, or neither yours nor SYSTEM — answers
NOT_FOUND.
Widened acceptance
Neither
fork_strategynor the create reads the source's stored revision snapshot. A strategy whose stored snapshot predates the current shape — every SYSTEM template among them — now forks and creates from its live state, where both were refused.
Contract history — v86 (a refusal hint belongs to the operation that can act on it)
Breaking at v86: refusals on most tools lose details.nextAct, two refusals change code, and one
draft read is reshaped. v86 is the contract on top of v85.
Narrowed refusal details
details.nextActfollows the refusal's meaning.DRAFT_VERSION_MOVEDandDRAFT_AXIS_CONTESTEDstill carryget_draftfrom every tool. A revisionCONFLICTand the in-flight commit refusal (get_draft) and a validation refusal (stage) now carry a hint only fromstage_<kind>_draftandcommit_<kind>_draftof the strategy and agent kinds.Removed from every other tool's validation refusal and revision
CONFLICT:submit_market_grid,fork_strategy,archive_strategy,restore_strategy, and the arena and radar commit, pause, resume and delete tools, and from the malformed-cursor refusal oflist_agent_draftsandlist_strategy_drafts. The arena and radarcommit_*_draftstale-revision refusals loseget_drafttoo. A client that branched onnextActfrom those tools reads the code instead.
Changed error codes
The strategy active-quota refusal is
FORBIDDEN, as the agent slot quota is, where it wasVALIDATION_ERRORwithnextAct: 'stage': oncommit_strategy_draftandrestore_strategy.A create naming an id that is already the caller's strategy or agent is
DRAFT_VERSION_MOVED, carrying the caller's draft version at that id, where it wasNOT_FOUND. An agent create naming a SYSTEM agent isFORBIDDEN, where it wasNOT_FOUND.The moved-draft refusals end "Read the draft again and show the player what it now holds before committing.", where they said to commit what it now holds. Read the draft again and show the player before committing; never commit the re-read draft on your own.
Reshaped output
get_agent_draft.impactis a union onoperation:{ operation: 'CREATE', capital }for a create draft, where it wasnull, and{ operation: 'UPDATE', deployedPresetCount, openPositionCount, radarArmedCoinCount, capital, rebind }for an edit draft, where the counts sat at the top level. A create draft'scapitalreads its trading configuration — drafted, or the platform's seed — over the bound strategy's band, as the commit's capital check reads it.
Widened read
get_strategywithincludeInactive: trueserves your own strategy in any state, and otherwise the visible set the default read serves — every SYSTEM strategy among it — where it answeredNOT_FOUNDfor anything but an owned PRIVATE strategy. Another user's PRIVATE strategy is stillNOT_FOUND.
Behaviour behind unchanged schemas
The stage and commit descriptions state the draft content cap (256,000 UTF-8 bytes) and its refusal, what a commit reaches — it closes no position, and the one refusal an open position can cause — and that a moved draft is shown to the player before any commit.
A stage's
DRAFT_VERSION_MOVEDfires only for a version the draft never reached — read from a draft since committed or discarded. A stage naming an older version than the draft holds is composed over the newer draft.
A strict client re-reads the get_agent_draft output schema; list the tools again after the server
deploys.
Contract history — v85 (agents and strategies change only through their drafts)
Breaking at v85: seven tools are retired, and a surviving tool refuses an input it accepted at v84. v85 is the contract on top of v84.
Every agent and strategy is now created and changed the same way: stage the change into the entity's draft, read the draft back with its diff, diagnostics and impact, and commit exactly the draft version and the live revision that read returned.
Removed tools
compile_strategy_plan,stage_strategy_plan,apply_strategy_plan,update_strategy_signal_rule,create_intelligence_agent,update_intelligence_agent,rebind_intelligence_agentare gone, with no alias; a call gets an unknown-tool error. The replacements:get_strategy_draft→stage_strategy_draft→commit_strategy_draftfor a strategy (a signal rule is a row of the draft'sSIGNAL_RULESaxis), andstage_agent_draft→get_agent_draft→commit_agent_draftfor an agent (a rebind is the draft'sSTRATEGY_BINDINGaxis).
Added tools
stage_strategy_draft { strategyId?, draftVersion, axes },commit_strategy_draftandcommit_agent_draft({ <id>, draftVersion, expectedRevision },expectedRevisionnull for a create). A commit publishes exactly the draft version and the live revision the caller read, is idempotent on{ id, draftVersion }, and returns the kind's committer response plus the draft'sdraftVersionafter its clear (null when the commit emptied it).
Rejected input
archive_strategyno longer takesconfirm. A request carrying it is refused with the unknown-key error — a break on a surviving tool.discard_agent_draftanddiscard_strategy_drafttakedraftVersion(the versionget_<kind>_draftreturned) in place ofconfirm;confirmis refused as an unknown key, and a version the draft moved past is refusedDRAFT_VERSION_MOVED.preview_strategy_reporttakes{ coinSelection, source }. The report fields move undersource: { kind: 'FIELDS', … }, andsource: { kind: 'DRAFT', strategyId, draftVersion }previews the owner's draft as a commit would compose it. A body with the fields at the top level is refused.A strategy's ENTRY axis is a union discriminated by
trigger:{ trigger: 'ON_CANDLE_CLOSE' }alone, or a level trigger withlevelOffsetAtrMultipleandvalidForBars.ON_CANDLE_CLOSEnaming either dial is refused, which the retired compile accepted; the server stamps the seed values.
Widened input
stage_agent_draftacceptsSTRATEGY_BINDINGon an edit draft, where v84 refused it: a staged binding commits as the agent's rebind. Its TRADING_CONFIG axis now describes each size preset as the risk budget lost at the hard stop.
Reshaped output
fork_strategyreturns{ strategyId, draftVersion }— the create draft it wrote, holding a copy of the source — in place of{ strategy }. No strategy exists until that draft commits.get_agent_draftandget_strategy_draftgaindraftVersion(0 when no draft is held, beside a nulldraft),committedRevision, a per-axisdiffagainst live,diagnosticsandimpact.stage_agent_draftgainsdiagnostics, the composed draft's errors and warnings.discard_agent_draftanddiscard_strategy_draftreturndiscarded: trueonly; a discard that finds nothing is refused rather than answeredfalse.
Widened enum
Strategy draft axes gain
ORIGIN, the create-only axis a fork writes, onlist_strategy_draftsand every strategy draft read. A client switching exhaustively on a strategy draft axis must add the branch.Error codes gain
DRAFT_VERSION_MOVED(detailsdraftVersion, null when no draft is held) andDRAFT_AXIS_CONTESTED(detailscontestedAxesanddraftVersion), both 409.Refusal details gain
nextAct. Every draft-specific refusal, the revisionCONFLICTand the in-flight commit refusal carrynextAct: 'get_draft'; a commit's validation refusal carriesnextAct: 'stage'; every other refusal carries none.details.expectedRevisionwidens tonumber | null.
Removed error code
PLAN_APPROVAL_NOT_FOUND, with the plan path that raised it.
Behaviour behind unchanged schemas
archive_strategyandrestore_strategyno longer refuse around the owner's unsaved draft: they re-base it onto the revision they write.restore_strategy's description drops the compile route — a repair is staged into the draft and committed, which restores the strategy.The
author-strategyprompt reads "Discover, stage, review, and commit a BattleGrid strategy through its draft".The server's manifest records a description hash per tool (manifest format 4), and the tool count moves 142 → 138.
A strict client that rejects unknown keys fails to parse fork_strategy and the draft reads at v85;
list the tools again after the server deploys.
Contract history — v84 (the hold moves from the condition to each clause and group)
Breaking at v84: a condition body accepted at v83 is refused, and condition evidence is reshaped. v84 is the contract on top of v83.
How many completed bars a reading must hold is no longer one number on the condition. Each clause holds its own reading, counted on its own column's timeframe, and a group can hold its members' same-bar reading.
Rejected input — compile_strategy_plan, apply_strategy_plan
A condition no longer carries
closes. A condition entry naming it is refused with the unknown-key error, in a plan body and in every normalized post-state and strategy draft those tools carry.Every clause and every group requires
hold: { atLeast, of }, whole numbers of at least 1.{ "atLeast": 1, "of": 1 }is one read (what every v83 condition did),{n, n}is "n closes in a row",{1, n}is "within n closes", and anything else is "at least m of n closes". A clause or group withoutholdis refused. A plan staged under v83 must be recompiled.What a header admits is refused by the builder, never by the schema.
atLeastaboveof, a window above the header's maximum, or a group hold over members that hold their own bars is a typed remediation error.
Replaced output — preview_strategy_report, list_strategy_vocabulary
closesReadable: booleanis replaced byconditionHold: { timeframe, maxWindow, maxAtLeast, refusal }on each report condition column and each scalar metric. It names the timeframe a clause counts on, the largestofandatLeastit admits (maxAtLeastis 1 for a cross or other event, so an event can only be held "within n closes"), and why it admits one bar when it does:NOT_REWINDABLE,DEVELOPING,OFFSET,BAR_STATEorHISTORY. Read the window from here rather than assuming a ceiling; there is no longer a fixed 1–5 limit.
Reshaped output — condition evidence
Every clause-evidence entry gains
hold,countsanddecidedBar, onpreview_strategy_report,list_gate_blocks,get_radar_close_decisions,get_agent_coin_qualification,preview_radar_resolutionandget_signal_log.countsis{ trueCount, total, unresolvedCount }over the clause's own bars.decidedBaris{ timeframe, closesBack }, the bar its operand was read on, ornullwhen the hold that counts it reads one bar.A condition outcome loses
hold(closesHeld,closesRequired,liveOutcome,nextCloseAt): the counted outcome is the clause's own, on its evidence. A decision record's conditiondeclarationlosescloses.Every strategy read returns
holdon each clause and group and nocloseson the condition.
Widened enum — the evidence union
The evidence union gains a
grouparm,{ kind: 'group', op, hold, counts, decidedBar, outcome, memberCount }, emitted only for a group holding more than one bar, just before thememberCountentries that belong to it. A client switching exhaustively onkindmust add the branch. A strategy without group holds serves no new arm.
A strict client that rejects unknown keys fails to parse condition evidence at v84; list the tools again after the server deploys.
Contract history — v70.1 (budget run-state)
Additive at v70.1: one output field added to the risk budget.
Added output — get_agent_budget, reset_agent_drawdown_baseline
budget.runStatusis your run-state for the agent:blocked,pausedoractive, already ranked by the server.blockedmeans the admission gate is refusing the agent for a reason other than its halt (blockedReasonnames it). A halted agent readspaused, even while its ownAGENT_HALTEDblock stands. Read it rather than re-deriving it fromblockedReasonandhaltedAt.A client that ignores the field is unaffected. A client that validates results against a cached, closed output schema should list the tools again after the server deploys.
Contract history — v83 (a model's vendor is its vendor slug)
Breaking at v83: four output fields renamed and retyped, one aggregation regrouped. v83 is the contract on top of v82.
Wherever a model's vendor is published, it is now the router vendor slug of the model's id — the part
before the / (anthropic, z-ai, moonshotai, …). It is never the BYOK provider enum, and never the
catalogue's display label, which sometimes named a hosting provider rather than the model's maker.
Reshaped output — the agent
Every agent loses
providerand gainsmodelVendorSlug: string, onlist_intelligence_agents,get_intelligence_agent,create_intelligence_agent,update_intelligence_agent,rebind_intelligence_agent,archive_intelligence_agentandactivate_intelligence_agent.providerwas the BYOK enum and wasnullfor every agent;modelVendorSlugis never null. A client readingproviderreads nothing at v83.
Renamed output — list_approved_models
Each model's
provideris renamedvendorSlug, and its value changes: the vendor slug (z-ai) where v82 served a display label that could name a host (StreamLake).
Renamed output, regrouped rows — get_agent_explorer
Each
modelVendors[]row'sprovider/providerImageUrlare renamedvendorSlug/vendorImageUrl.Rows group by vendor slug, so every model one vendor makes shares a row whatever its display label: three GLM labels that were three rows at v82 are one
z-airow at v83. Counts and sums add; each mean is the merged total over the merged trade count.
Renamed output — the ownerView LLM-call envelope
The envelope's
provideris renamedmodelVendorSlug, onget_agent_journal,get_signal_log,get_agent_game_history,get_user_agent_game_history,get_public_agent_signal_log_detailandget_public_agent_game_history. It isnullexactly whenmodelDisplayNameis — a call whose model has left the catalogue.
Contract history — v82 (approved models lose their pin flag)
Breaking at v82: one output field removed. v82 is the contract on top of v81.
Removed output — list_approved_models
pinProvideris gone from every modellist_approved_modelsreturns. A model is now served only by an ordered list of verified hosts that the server keeps to itself, so a pin flag no longer describes anything. There is no alias. A client that readspinProviderreads nothing at v82, and a strict client that requires it fails to parse the response.
Contract history — v81 (the allocation's committed figure removed, the hub names Max exposure)
Breaking at v81: one output field removed and one renamed. v81 is the contract on top of v80.
Removed output — the agent fund allocation
committedUsdis gone from the allocation thatget_agent_fund_allocation,halt_intelligence_agent,resume_intelligence_agentandset_agent_per_trade_pushreturn. It measured a custody ledger whose allocate and recall routes are retired. The figure that bounds an entry is capital at risk, onget_agent_budget. A client readingcommittedUsdreads nothing at v81.
Renamed output — get_agents_hub
Each agent's
envelopenames the exposure ceilingmaxConcurrentExposureUsd, the trading config's own name for it, where v80 called itbudgetUsd. The value is unchanged. A client readingbudgetUsdreads nothing at v81.
Corrected reason behind an unchanged schema
An agent entry refused because its Max exposure is fully committed is recorded as
INSUFFICIENT_BALANCE, where it was recorded asBELOW_EXCHANGE_MINIMUMwith a $0.00 risk budget. Its detail names Max exposure and the capital at risk. It reachesget_signal_logandget_public_agent_signal_log_detail.
Contract history — v79 (the agent's account reading, an exhausted budget, accept below the Balance floor)
Breaking at v79: the agent budget's account fields move into one object.
Reshaped output — the agent budget
get_agent_budgetandreset_agent_drawdown_baselinereplaceaccountEquityUsd,budgetOverSubscribedandopenUnrealizedPnlUsdwith oneaccountobject carrying the same three fields, ornullwhen the server holds no cached reading of the account. At v78 an unread account read0,falseand0, which looked like a real empty account. A client reading the three top-level fields reads nothing at v79.
Widened enum
The block reason gains
EXPOSURE_BUDGET_EXHAUSTED(owner-private): an agent whose capital at risk has reached its Max exposure is blocked once at the account stage, where v78 blocked it per coin asEXCHANGE_MIN_NOTIONAL_UNREACHABLE. It reaches every output that carries a block reason:list_gate_blocks,get_signal_log,get_public_agent_signal_log_detail,get_radar_activity,get_radar_close_decisions,get_radar_deployment,list_radar_deployments,preview_radar_resolution,get_agent_coin_qualification,get_trade_conversationandpropose_entry_decision.
Refused behind an unchanged schema
accept_entry_decisionrefuses withCONFLICTwhile your account equity is below the agent's Balance floor (balanceThresholdUsd) or cannot be read. The decision stays pending and can be accepted once the equity is back at or above the floor.
Contract history — v78 (a new agent's risk policy from one seed)
Breaking at v78: get_trading_config_catalog serves a new agent's risk policy as one object.
Reshaped output
tradingDefaults.defaultsreplaces ten per-field seed values —defaultMaxDailyTrades,defaultMaxLeverage,defaultSmallPct,defaultMediumPct,defaultLargePct,defaultEntrySlippageBps,defaultMaxConcurrentExposureUsd,defaultBalanceThresholdUsd,defaultMaxCumulativeDrawdownUsdanddefaultMaxDailyLossUsd— with oneagentTradingConfig, request-shaped, so it passes tocreate_intelligence_agentverbatim. A client reading the ten fields reads nothing at v78.
Input schema
tradingConfig.maxLeverageloses its publishedminimum: 1oncreate_intelligence_agentandupdate_intelligence_agent. Acceptance is unchanged: the platform's minimum leverage is the bound.
Wider input
Both tools accept a strategy whose stored dials sit outside today's operator bounds. An agent write never re-judges a strategy dial.
Served values
A
create_intelligence_agentcall withouttradingConfigis seeded from the same object the catalog serves, never from column defaults, with Min order size from the platform minimum.
Contract history — v77 (agent risk rules: each loss stop on its own rail)
Breaking at v77: an agent's tradingConfig accepts less. create_intelligence_agent and
update_intelligence_agent refuse values v76 stored, and one loss stop moves to a different bound.
Rejected input — tradingConfig
maxLeveragemust be a whole number.1.5was stored and executed at1; it is now refused.maxSlippageBpsis at most300, the entry slippage ceiling no fill could exceed anyway.USD fields and the three size presets take at most 2 decimals.
maxDailyLossUsdmust be at or belowmaxCumulativeDrawdownUsd.A body accepted at v76 can be refused at v77 without one byte of it changing. Each refusal names its field in a plain sentence, such as
Whole numbers only.orCan’t exceed the drawdown stop ($20.00).
Wider input — the drawdown stop
maxCumulativeDrawdownUsdis bounded by the platform's drawdown maximum, not bymaxConcurrentExposureUsd. Max exposure caps margin open at once; the drawdown stop caps loss over time. A$150drawdown stop on a$100max exposure is accepted.
Wider output
get_trading_config_cataloggainsmaximumMaxCumulativeDrawdownUsd, the drawdown stop's upper bound.The execution-failure reason gains
BELOW_AGENT_MIN_ORDER(owner-private) onget_signal_logandget_public_agent_signal_log_detail: an order sized under the agent's ownminAllocationUsd, which was reported asBELOW_EXCHANGE_MINIMUM.
Served values
get_agent_budget'stradesTodaycounts entries filled since 00:00 UTC plus entries still waiting to fill. Refused and expired entries no longer count, and the cap is enforced where an entry is created.DAILY_LIMIT_RESERVEDis no longer emitted.A daily-loss halt whose drawdown stop is also breached becomes a drawdown halt when the day rolls over, rather than lifting.
A capital-feasibility refusal names what clears it: the
maxConcurrentExposureUsdthat would, and, when one at or above the platform minimum would, a lowerminAllocationUsd.
Contract history — v76 (the extreme funding signals in percent a year)
Breaking at v76: funding_extreme_positive and funding_extreme_negative take their threshold in
percent a year. Their parameter is thresholdAnnualizedPct, the unit of the strategy grammar's
ann column, where it was thresholdPct, a fraction of one hourly funding settlement.
Rejected input — the retired thresholdPct
Both signals take
thresholdAnnualizedPct, bounded 1–43,800 and defaulting to 100 — the funding label's cryptoextremeband, so100means 100% a year, and the negative signal fires below -100%.thresholdPcton either signal is refused asVALIDATION_ERRORwherever a rule's parameters are authored:compile_strategy_plan,update_strategy_signal_rule,derive_strategy_rule_viewand a strategy draft'sSIGNAL_RULESaxis.oi_surgekeeps its ownthresholdPct.A body accepted at v75 can be refused at v76 without one byte of it changing. A rate per hourly settlement converts to percent a year by × 8,760 × 100:
0.0002is175.2.
Stale plans
A plan token compiled before the deploy fails as a stale plan at
stage_strategy_planandapply_strategy_plan, because the authoring catalog digest covers each signal's parameters, defaults and explanation: compile again. The strategy preview'svocabularyDigestmoves too.
Served values
get_strategy_signal_definitionandlist_strategy_signalsserve the new parameter, its default and its explanation.Each signal's indicator values gain
funding_annualized_pct, the annualized rate the signal compared, besidefunding_rate.Stored rules move to the new key after the deploy: a rule at the retired default takes
100, and any other converts exactly.get_coin_signal_previewand every stored rule then score at the new threshold.No schema hash moves. Signal parameters are published as a JSON object on every input and output, so the change is in the values accepted and served, and
toolCountis unchanged.
Contract history — v75 (readability from each coin's data profile)
Breaking at v75: the seven tools v73 narrowed refuse by the coin's market-data profile, and one output vocabulary gains a member. Which data a coin carries is now the profile the market-data service publishes for that coin, no longer the coin's asset class.
Rejected input — the seven tools v73 narrowed
A condition reading a kind of data the coin's profile does not serve is refused as
INSTRUMENT, on the coin or on a benchmark section's own instrument: a spot read on a crypto coin no spot venue lists is refused as one on a TradFi coin always was.A coin the market-data service publishes no profile for refuses every condition that reads market data: a coin enabled since the service's last boot, a roster coin whose candle stream it did not subscribe, a ticker with no catalog row.
The tools, codes and
details.contextare v73's:preview_radar_resolution(every kind, issuing nopreviewToken);commit_radar_deployment_draft,rebind_intelligence_agent,compile_strategy_planandrestore_strategy(VALIDATION_ERRORwithCONDITION_UNREADABLE_BY_RADAR_SCAN);apply_strategy_plan, which applies only what its compile checked; andpropose_entry_decision(CONDITION_UNREADABLE_ON_COIN).A body accepted at v74.1.1 can be refused at v75 without one byte of it changing. A write whose read of the published profiles fails is refused, never admitted unchecked.
Reclassified reason — FEED becomes INSTRUMENT
A spot read for a ticker with no catalog row is
INSTRUMENT, where it wasFEED, and an uncatalogued benchmark refuses every market-data read, where only its spot reads refused before.The
INSTRUMENTmessage names what the profile lacks: "<instrument>'s market-data profile carries no<kind>data", or "the market-data service publishes no data profile for<instrument>, so it carries no market data", where it read "<instrument>has no spot tape".The readings v74 added report it as the sweep does:
preview_radar_resolution'sreadingsandget_agent_coin_qualification'sreadingmark a condition on a kind the coin's profile lacksINSTRUMENT.
Wider output — BarFamily gains MARKET_STATS
The market-stats snapshot is a per-bar value: a decision reads the snapshot fetched inside the
1mbar ending at its close. A snapshot missing there is a per-bar shortfall, afamiliesentry or a MISSED close'sINPUTS_OFF_BARlackingentry namingMARKET_STATSat1mwith itsdueBarStart,ABSENTorGONE, and the decision waits or misses as for any per-bar family.A decided close records no
MARKET_STATSstale sample. A reading at the latest values still does, and itsboundMsis the coin's market-stats freshness target, never below 300,000 ms.Five outputs publish the member:
get_agent_coin_qualification,get_radar_close_decisions,get_signal_log,list_gate_blocksandpreview_radar_resolution. No input takes the vocabulary. A client that validates results against a cached output schema should list the tools again after the server deploys.
Contract history — v74.1.1 (a same-agent request answered from what stopped the fire)
Not breaking: one answer corrected behind unchanged schemas.
A request on a pair your own radar policy decides with the same agent is answered from that policy's decision. When the policy's fire enqueued no trade decision and no admission gate refused it — a coin cooldown, the hourly fire cap, post-close suppression, a decision already running on the coin, a deduplicated fire, or a fire aborted at its re-read — the conversation gains the card the request's own unfired fire gets:
close_claimed_by_radarwhen another agent's fire holds the coin, otherwiseclose_refusedwith the reading's score against its minimum. It gainedclose_fired_by_radarbefore, though no trade was taken.Both cards are already in
get_trade_conversation's vocabulary, so no served shape moves.
Contract history — v74.1 (missed-bar alerts and the entry pause)
Not breaking: four output vocabularies gain a member, and propose_entry_decision refuses while an
entry pause is open. While bars missed for lack of market data span many coins, the platform pauses
every new entry the radar decides, a radar fire and a manual request alike, and the pause lifts by
itself after a quiet period.
Refused while a pause is open — propose_entry_decision
A request is refused after its own checks and before a watch is registered, whatever the agent or coin:
type: "error"with{ origin: "ENGINE", reasonCode: "ENTRIES_PAUSED" }. The sameidempotencyKeyreplays the refusal for five minutes; ask again under a fresh key once the pause lifts.A request queued before the pause whose close qualifies while it is open gets the same ENGINE error card in its conversation, and no decision is proposed.
accept_entry_decisionon a decision already proposed, and every exit, run as without a pause.
Wider output — four vocabularies
TradeEvaluationAttemptReasonCodegainsENTRIES_PAUSED: the refusal'sreasonCode,list_gate_blocks'reasonCode, and thescanBlockReasonof a refused fire's close record and journal row.TradingPipelineGateStagegainsPLATFORM: alist_gate_blocksentry for each fire the pause refused, with no coin, no detail and no decision record.RadarFireDispositiongainsEDGE_PRESERVED_ENTRIES_PAUSED: the refused fire keeps its edge, so the pair fires at a later close only if that close qualifies on its own data. It is served onget_radar_activityandget_radar_activity_summary, and as a close record'soutcome.fireDispositionbesideoutcome.scanBlockReason: "ENTRIES_PAUSED", withoutcome.outcomestayingFIRED.RadarIdleReasongainsENTRIES_PAUSED: theresolvesNow.reasonof everySCANNINGcard onget_radar_deploymentandlist_radar_deploymentswhile a pause is open. The section staysSCANNING, because closes are still decided and recorded.
Answer corrected — a same-agent request
When an admission gate refused your own policy's fire — an entry pause, an account block, a position already held on the coin — the request's conversation gains that block's ENGINE error card instead of
close_fired_by_radar.
Contract history — v74 (preview conditionReach replaced by readings)
Breaking at v74: one required output field replaced on one tool, and an opt-in reading added to another. Both now serve what an agent reads on a coin, in the decision-record format v73.3 introduced.
Reshaped output — preview_radar_resolution
conditionReachis gone, replaced byreadings: one entry per on-duty agent, in on-duty order, each namingagentIdandagentDisplayName. Akind: "READ"entry carries the agent'sreading, the oneget_agent_coin_qualificationserves (below). Akind: "UNSCORABLE"entry, an agent whose evaluation was rejected, carriescoinDataStoppedinstead: the coin's stopped-data reading, or null. One agent's rejection is its own entry, never a failed preview. The tool always asks for readings, soreadingsis null only under asimulatedRegime.blocksScanGatehas no successor. Since v73 the preview refuses a slot agent with a condition the radar scan cannot read on the coin, so no previewed pair holds a permanently blocking condition. Whether the conditions hold the coin is the reading's own gate,reading.live.reading.qualification.gates.requiredConditions.Read
reachReasonfrom the reading. EachREADentry'sreading.live.reading.conditions.entries[]lists the conditions: akind: "UNEVALUATED"entry carries the condition'sconditionKey,nameandreachReason, and akind: "EVALUATED"entry, the counterpart of a nullreachReason, carries its outcome with its clause values.declaration.requiredmarks the required ones, the setconditionReachcovered.A client that validates results against a cached output schema fails the call until it lists the tools again, because the old schema required
conditionReach. Reconnect, or re-list, after the server deploys.
Wider input and output — get_agent_coin_qualification
reading(boolean, default false) asks for the pair's reading, and each verdict gains the required-nullablereading, null unless asked. A call without the flag is answered as before, withreading: null.reading.liveis the reading the verdict was built from, withanchorBarStatus(livewhile the anchor bar still forms),scoredBarStart(the bar the signals and gates were scored on) andobservedAt. It is what the agent reads now, not a decision: the radar decides at the close.reading.lastCloseis the deciding reading of the bar the agent's on-duty row names as its last close decision:state: "RECORDED"with that bar's close-decisionrecord,state: "PENDING"with itsbarStartwhile that record is being written, orstate: "NONE".get_radar_close_decisionsholds the decisions before it.
Contract history — v73.3 (decision records)
Purely additive: one read tool, one evaluation-attempt reason and four output fields added. What a decision read is now served: the radar's close decisions on a coin, the report behind a trade, and the compose decision behind a gate block.
Wider surface — one tool added
get_radar_close_decisions(coinId; optionalagentId,before,limit;mcp:read) — one coin's radar close-decision records, newest decided bar first: every bar an agent decided —FIRED,CLAIMED,NOT_QUALIFIEDorMISSED— with the reading it was decided on (a miss's answer instead), its fire disposition and its versions. Your own records only: a SYSTEM agent's records on a coin belong to every user who deployed it. A page never splits a bar, so it can run pastlimit; to page back, pass the oldest record'sbar.barStartasbefore.toolCountgoes 141 → 142.
Wider output — the risk budget, Radar reads, the trade conversation, gate blocks and signal logs
TradeEvaluationAttemptReasonCodemay readSCREENED_OUTonget_agent_budget,reset_agent_drawdown_baseline,get_radar_activity,get_radar_deployment,list_radar_deployments,preview_radar_resolution,get_trade_conversation,propose_entry_decision,list_gate_blocks,get_signal_logandget_public_agent_signal_log_detail: compose's deterministic pre-model screen found no routable direction. That refusal now writes a gate block. It is public: the public profile carries it as is. A client holding its own closed copy of the enum rejects the new member; one that renders unknown reasons generically is unaffected.get_signal_log'sloggainsreportSections(the report the model read, section by section, in the shape ofpreview_strategy_report'srenderedSections),promptDataSerialization(the serialization it was rendered under) andmissingData(the data compose lacked), each null for a trade recorded before capture.get_public_agent_signal_log_detailcarries none of them.Each
list_gate_blocksentry gainsdecisionRecord: the compose decision record written with the block — its outcome, its evidence (the reading compose computed, the per-bar values a refused decided bar lacked, or none), its missing data and its versions. It is null for a block written before capture and for a block raised before compose.
Contract history — v73.2 (decided-bar refusal)
One member added to the evaluation-attempt reason, published as output only, and two detail fields added to gate blocks. A confirming-close fire now names the bar it decided, and compose reads that bar or refuses with the new reason, instead of reading the newest completed bar at its own instant.
Wider output — the risk budget, Radar reads, the trade conversation, gate blocks and signal logs
TradeEvaluationAttemptReasonCodemay readDECIDED_BAR_UNAVAILABLEonget_agent_budget,reset_agent_drawdown_baseline,get_radar_activity,get_radar_deployment,list_radar_deployments,preview_radar_resolution,get_trade_conversation,propose_entry_decision,list_gate_blocks,get_signal_logandget_public_agent_signal_log_detail: compose refused a radar fire at the conditions stage, before any LLM call, because a per-bar value its conditions gate reads was not on its due bar for the bar the radar decided — its entry had not landed, or had gone. It is public: the public profile carries it as is. A client holding its own closed copy of the enum rejects the new member; one that renders unknown reasons generically is unaffected.list_gate_blocks'reasonDetailgainsdecidedBarStart(ISO 8601, the decided bar's open) anddecidedBarTimeframe(the strategy timeframe), present on that reason alone.
Contract history — v73.1 (strategy card fields)
Purely additive: thirteen read-only fields added to every list_strategies row. The input is
unchanged, and a client that ignores the fields is unaffected.
Wider output — list_strategies
The strategy card:
regimeTimeframeandlowerTimeframe(the derived ladder rungsget_strategyalready serves);conditionTally,signalRuleTallyandexitTally(armedout oftotal, the four exit mechanisms); andminRequiredCount,entryTrigger,minRiskRewardRatioandmaxStopLossAtrMultiple.forkedFromStrategyName, null when the strategy is not a fork or its source is neither yours nor a SYSTEM strategy.viewerBoundAgentCountandviewerOpenPositionCount, your own agents on the strategy and their open positions.boundAgentCountstill counts every user's agents; read the viewer fields for yours.viewerPerformance, your own all-time results on the strategy, null when you never traded it. A trade counts toward the strategy recorded on the signal log that opened it, so rebinding an agent never moves it.
Contract history — v73 (unreadable radar conditions refused)
Breaking at v73: seven tools refuse bodies they used to accept, behind unchanged input schemas. A condition the radar acts on — a required one, a direction-setting one (a verdict carrier), an exit one, and every condition they reference — must be readable by the radar scan on every coin the radar acts on for its agent: each coin a Radar policy slots it on, enabled or paused; each coin it holds an open position on, for its exit conditions; and each coin it has a pending manual request on.
Rejected input — refused behind unchanged schemas
VALIDATION_ERRORwith the authoring codeCONDITION_UNREADABLE_BY_RADAR_SCANfrompreview_radar_resolution(every kind, for every slot agent whether on duty now or not, issuing nopreviewToken),commit_radar_deployment_draft,rebind_intelligence_agent, andcompile_strategy_planandrestore_strategyon behalf of the bound agents the radar acts on (a strategy none of whose agents holds a slot, an open position or a pending request is never checked).details.context.reachReasonsays why:INSTRUMENT(the coin, or a benchmark section's own instrument, has no such data),AGENT_TIMEFRAME(the agent has no such rung) orFEED(the radar scan never reads that data: crowd reads, a spot read for an uncatalogued ticker, a timeframe the agent's plan does not load).The message names the first conflicting agent, the condition, the column, the coin and the fixes: stop the radar acting on the condition, make it read a column the scan has on that coin, or free the coin — take the agent off that coin's policy, close the position, or cancel the pending request. It is a fact about the pair, which no later sweep changes.
A refused compile issues no plan token, so
stage_strategy_plancannot stage that content, andapply_strategy_planapplies only a plan whose bound agents still hold the radar slot coins its compile checked: a plan compiled before a deploy moved them fails verification. Compile again.A condition reading a Session Field scalar keeps
CONDITION_OPERAND_UNSERVED_IN_LANE, now refused at the rebind and every strategy revision too, not only at the deploy.stage_radar_deployment_draftis unchanged: a draft holds no slot, and the preview its commit needs refuses. A body accepted under v72 is refused under v73 without one byte of it changing.
Wider output — propose_entry_decision, get_trade_conversation
propose_entry_decisionrefuses a request whose agent the radar scan cannot read on the requested coin, before its watch is registered, and releases its idempotency claim. It returnstype: "error"witherror: { reasonCode: "CONDITION_UNREADABLE_ON_COIN", unreadable: { conditionKey, conditionName, column, coinTicker, reachReason } }: facts only, no prose. Choose another coin, another agent, or a condition reading a column the radar reads on that coin, and send it again.get_trade_conversation's error card carries the samereasonCodeandunreadable.A client that validates results against a cached output schema fails a call that returns the new error until it lists the tools again, because the old schema has no such member. Reconnect, or re-list, after the server deploys.
Refusals worth knowing before you apply, restore or rebind
CONFLICTwithdetails.reason: "RADAR_DEPLOYMENT_MOVED"fromapply_strategy_plan,restore_strategyandrebind_intelligence_agent: the agents' radar slot coins moved after the write's check. Re-read and retry, so the retry's check covers the moved deployment; for a plan, compile again.commit_radar_deployment_draftis aCONFLICTwhen a slot agent's strategy or timeframes changed after its check. Preview again.
Contract history — v72.0.1 (account read faults withheld on the public profile)
No schema change: one value reclassified on one public read.
Reclassified output — get_public_agent_signal_log_detail
pipeline.attempt.reasonCodescarriesOWNER_PRIVATE, with its detail omitted, where it would have carriedEQUITY_CHECK_UNAVAILABLE,ALLOCATION_CHECK_UNAVAILABLE,DAILY_COUNT_CHECK_UNAVAILABLEorAPPROVAL_CHECK_UNAVAILABLE. Each says the account has an open block, which the public profile withholds. This supersedes the v71.1 note below that the public detail carries the two faults v71.1 added.OWNER_PRIVATEhas been in that field's schema since v71, so a client validating against a cached output schema accepts it.get_signal_logstill carries every precise code on your own agents.
Contract history — v72 (public decision userId removed)
Breaking at v72: one output field removed from one public read.
Removed output — get_public_agent_signal_log_detail
log.linkedEntryDecision.userIdis gone. It was the user the agent acted for. On a SYSTEM agent, which has no author and which any user may deploy, that named whoever deployed it, and the public profile deliberately shows no owner for a SYSTEM agent. There is no replacement path on the public read. A PRIVATE agent's author isownerUserIdon the agent's public profile, and the owner toolget_signal_logstill carrieslinkedEntryDecision.userIdon your own agents.A client that validates results against a cached output schema fails the call until it lists the tools again, because the old schema required the field. Reconnect, or re-list, after the server deploys.
Contract history — v71.1 (two read-fault refusals)
Two members added to the evaluation-attempt reason, published as output only. When the server cannot read an agent's daily trade count or its trading account's approval, the admission is blocked with its own reason instead of borrowing the verdict it could not reach.
Wider output — the risk budget, Radar reads, the trade conversation, gate blocks and signal logs
TradeEvaluationAttemptReasonCodemay readDAILY_COUNT_CHECK_UNAVAILABLEorAPPROVAL_CHECK_UNAVAILABLEonget_agent_budget,reset_agent_drawdown_baseline,get_radar_activity,get_radar_deployment,list_radar_deployments,preview_radar_resolution,get_trade_conversation,propose_entry_decision,list_gate_blocks,get_signal_logandget_public_agent_signal_log_detail.DAILY_TRADE_LIMIT_REACHEDandAGENT_APPROVAL_EXPIREDstill mean what they say. A client holding its own closed copy of the enum rejects the new members; one that renders unknown reasons generically is unaffected.
Contract history — v71 (owner-private reasons on the public profile)
Breaking at v71: two public inputs narrow, and one public output widens.
Rejected input — get_public_agent_signal_logs, get_public_agent_realized_trades
filter.rejectionReasonrefuses the six owner-private failure reasons —BELOW_EXCHANGE_MINIMUM,INSUFFICIENT_BALANCE,INSUFFICIENT_MARGIN,AGENT_APPROVAL_EXPIRED,SIGNING_KEY_UNAVAILABLE,AGENT_HALTED— with a validation error, where they used to filter. These tools read another user's agent, and a private reason answered a question about that user's budget, margin, approval, key or halt. No alias exists; filter by a public reason.filter.expiryReasonstill accepts every member.
Wider output — get_public_agent_signal_log_detail
pipeline.attempt.reasonCodes,pipeline.execution.failureReasonand.expiryReasonmay readOWNER_PRIVATEin place of an owner-private reason, whose detail is then omitted. A client holding its own closed copy of these enums rejects the new member; one that renders unknown reasons generically is unaffected. A failed execution'sexecutionMessageis now always null.get_signal_logis unchanged: on your own agents it still carries every precise code, its detail and the narrative.
Contract history — v70.2 (allocation-check refusal)
One member added to the evaluation-attempt reason, published as output only. An admission the
server blocked because it could not read the account's allocation now carries
ALLOCATION_CHECK_UNAVAILABLE; NO_AGENT_ALLOCATION survives only on historical rows.
Wider output — the risk budget, Radar reads, the trade conversation and gate blocks
TradeEvaluationAttemptReasonCodemay readALLOCATION_CHECK_UNAVAILABLEonget_agent_budget,reset_agent_drawdown_baseline,get_radar_activity,get_radar_deployment,list_radar_deployments,preview_radar_resolution,get_trade_conversation,propose_entry_decisionandlist_gate_blocks. A client holding its own closed copy of the enum rejects the new member; one that renders unknown reasons generically is unaffected.The reason detail no longer declares
availableUsdorrequiredUsd(get_signal_log,get_public_agent_signal_log_detail). No response ever carried them.
Contract history — v70 (risk-gauge configured removed)
Breaking at v70: one output field removed from every risk-budget gauge.
Reshaped output — get_agent_budget, reset_agent_drawdown_baseline
budget.gauges.<gauge>.configuredis gone from all four gauges (dailyTrades,exposure,drawdown,dailyLoss). It could only readtrue: the daily-trades cap is at least 1, and an agent's exposure ceiling, drawdown stop and daily-loss stop are all required and strictly positive. Every gauge has a limit, so readfill,remainingandbreachedand render every meter enabled.A client that validates results against a cached output schema fails the call until it lists the tools again, because the old schema required the field. Reconnect, or re-list, after the server deploys.
Contract history — v69.7 (insufficient-margin refusal)
Purely additive: one member added to the trade-failure reason, in both directions. An agent entry
the shared trading account cannot post the isolated margin for is now refused before any order is
sent, with its own reason. The same reason replaces EXCHANGE_ERROR when the exchange refuses an
order for margin first.
Wider input — get_public_agent_signal_logs, get_public_agent_realized_trades
filter.rejectionReasonacceptsINSUFFICIENT_MARGIN, where it used to refuse it.
Wider output — get_signal_log, get_public_agent_signal_log_detail
failureReasonmay readINSUFFICIENT_MARGIN. A client that switches exhaustively on the reason needs one more arm; one that renders unknown reasons generically is unaffected.
Contract history — v69.5 (liquidation-buffer refusal)
Purely additive: one member added to the trade-failure reason, in both directions. An agent entry whose hard stop no whole leverage can keep clear of liquidation is now refused before any order is sent, with its own reason. Below that, an agent order's reported effective leverage may sit under the agent's bound: it is lowered until the isolated liquidation is at least twice as far from the fill as the hard stop.
Wider input — get_public_agent_signal_logs, get_public_agent_realized_trades
filter.rejectionReasonacceptsLIQUIDATION_BUFFER_UNREACHABLE, where it used to refuse it.
Wider output — get_signal_log, get_public_agent_signal_log_detail
failureReasonmay readLIQUIDATION_BUFFER_UNREACHABLE. A client that switches exhaustively on the reason needs one more arm; one that renders unknown reasons generically is unaffected.
Contract history — v69 (Radar multi-agent duty)
Breaking: one input removed from two tools, three outputs reshaped. A Radar policy carries no timeframe. Every rule whose conditions match puts its agent on duty at once, and each rule's regime condition is read at that agent's own regime timeframe. A coin still holds one position at a time: at most one fire per coin per pass, offered in rule priority.
Rejected input — something you send is no longer accepted
preview_radar_resolution'sSLOTSrequest refusesdeploymentTimeframeas an unrecognized key. Send{ kind: "SLOTS", slots }.stage_radar_deployment_draftrefuses theDEPLOYMENT_TIMEFRAMEaxis the same way. Stage onlyRULESandDEFAULT_SLOT.
Reshaped output — something you read has a new shape
resolvesNowonget_radar_deployment,list_radar_deploymentsandpreview_radar_resolutionis the coin's state (section,isIdle,rotating, onereason, the open-position owner) plusonDuty: one row per agent on duty, in priority order. Each row carries its slot, its regime reading at its own regime timeframe, its qualification verdict and gate, cooldown, block,edgeSpent, last flip and fire, and its owncloseDecision. The single-winner fields (onDutyAgentIdand its display pair,matchedSlot*,regimeUsed, and the per-agent fields the rows now carry) are gone. Render every row in the order served, and never pick one yourself.preview_radar_resolution'sconditionReachis one flat list, each entry naming itsagentIdandagentDisplayName.Each deployment slot gains
agentRegimeTimeframe, the timeframe its rule's regime is read at.
Removed output — a field you read is gone
deploymentTimeframeon a Radar policy, and theDEPLOYMENT_TIMEFRAMEaxis in the radar draft'scontentandaxesMetaonget_radar_deployment_draft,list_radar_deployment_draftsand the stage result.
Wider enums — a value you may now receive
closeDecision.outcomegainsCLAIMED: the agent's close qualified, but a higher-priority agent took the coin's one fire that pass. Its edge is preserved, and it fires at a later close only if it still qualifies there.get_radar_activityandget_radar_activity_summarygain the eventsON_DUTY_JOINEDandON_DUTY_LEFT(duty is journaled per agent) and the fire dispositionEDGE_PRESERVED_COIN_CLAIMED.curveDigestdescribes the first agent on duty, named incurveAgentName.get_trade_conversationmay carry theclose_claimed_by_radarcard.
The exported battlegrid-radar-deployment skill describes the v69 flow.
Contract history — v68.1 (Arena regime sets)
Purely additive in schema, with one refusal behind unchanged ones. An Arena rule's regime condition is a SET in the draft, as it always was in the committed deployment: one rule can fire in several regimes, instead of one rule per regime.
Wider input — stage_deployment_policy_draft
A rule's
regimesmay name several regimes, where the draft capped it at one. The set is stored in the selectable-regime order, so a staged[VOLATILE, BULL_EXPANSION]reads back[BULL_EXPANSION, VOLATILE]. A regime named twice is refused.
Wider output — get_deployment_policy_draft
The draft may return such a set.
get_deployment_policyhas served sets all along, so a client that reads a committed deployment already handles them.
Refusals behind unchanged schemas
preview_deployment_resolution(SLOTS),preview_radar_resolution(SLOTS) andtest_generate_deployment_gridrefuse a regime condition naming one regime twice, asVALIDATION_ERRORat itsregimesfield, where they used to resolve or generate over it. The stage tools already refused a repeat, so no commit changes; a repeated member is a malformed set.
Contract history — v68 (Arena drafts)
Breaking: one tool retired, two inputs reshaped. Arena deployment content now commits only as a draft the player was shown a live preview of, as radar content has since v66. The player's deploy editor and every conversation share one unsaved draft per arena.
Removed tool — something you call no longer exists
upsert_deployment_policyis retired, with no alias. Calling it is an unknown-tool error. To deploy or change an arena's deployment:stage_deployment_policy_draftthe axes you are changing (RULES,DEFAULT_SLOT,REGIME_ANCHOR), with thedraftVersionyour last read returned;preview_deployment_resolutionwithrequest: { kind: "DRAFT", draftVersion }at the version staging returned;show the player the resolution and whether it will play (
enabledAfterCommit), and on their word callcommit_deployment_policy_draftwith thepreviewTokenthat preview returned andconfirm: true.
Pausing is no longer a flag on the write: use
pause_deployment_policy. A first deployment always plays; a replacement keeps its pause.
Rejected input — something you send is no longer accepted
preview_deployment_resolution'srequestneeds akind.{ kind: "SLOTS", slots, … }is the old request.{ kind: "DRAFT", draftVersion }previews the player's draft of the arena at that version, composed over the deployed policy exactly as the commit writes it.{ kind: "COMMITTED" }previews the deployed policy a resume would arm. A request withoutkindis refused.delete_deployment_policyrequiresexpectedPolicyIdbesideexpectedRevision: a revision restarts at 1 when an arena is redeployed, so only the pair names the deployment you read.A regime anchor override names its coin and timeframe together, or neither, on every path including
test_generate_deployment_gridand the SLOTS preview.
Wider surface — eight tools added
stage_deployment_policy_draft,get_deployment_policy_draft,list_deployment_policy_draftsanddiscard_deployment_policy_draft(mcp:read). None of them commits or arms anything.commit_deployment_policy_draft(presetId,previewToken,confirm: true;mcp:wager) — the only MCP committer of Arena content.pause_deployment_policy(presetId;mcp:wager) — no revision and no certificate.resume_deployment_policy(presetId,previewToken,confirm: true;mcp:wager) — needs the certificate a liveCOMMITTEDpreview returned.cancel_market_grid_submission(sessionId,confirm: true;mcp:wager) — the player's own cancellation and refund of one entry.
Reshaped and wider output
preview_deployment_resolutionnests the resolution underresolutionand gainspreviewToken(the certificate a live DRAFT or COMMITTED preview earns, null otherwise, good for five minutes) andenabledAfterCommit.pause_deployment_policyanddelete_deployment_policyreturnopenEntries: the entries the player's agent already made in the arena's pending sessions, each with its lock time and entry fee. They play out unless the player cancels them — tell the player and ask, then callcancel_market_grid_submissiononly for the entries they pick.get_deployment_policy'sauthoringContextgains the arena's header facts (regimeReferenceTicker,presetBadgeImageUrl,entryFee, andplayerCount— null when the arena has no pending session to count).The thought-log
outcomegainsSKIPPED_DEPLOYMENT_DISARMED: an entry job that found its deployment withdrawn, paused or no longer slotting its agent stood down before paying.
Refusals worth knowing before you commit or resume
The certificate is bound to what you previewed: your credential, the player, the arena, the subject, the deployment and its revision, the draft version and the composed content. Anything that moves is a
CONFLICTand nothing is written. Preview again; never reuse the old certificate.A commit never withdraws: a draft with no rule and no catch-all is refused, naming
delete_deployment_policy.Resume is refused on a retired arena and on one the player's access was revoked from; pausing and withdrawing stay open.
Deployment refusals answer
INVALID_DEPLOYMENT_POLICY.delete_deployment_policyalso ends the player's draft of the arena.
Vocabulary
toolCount goes 134 → 141 (eight added, one retired). preview_deployment_resolution's input and
output, delete_deployment_policy's input and output, get_deployment_policy's and
list_deployment_policies' outputs, the three journal outputs and test_generate_deployment_grid's
input (a session start now accepts up to 288 times a day) move; upsert_deployment_policy's are
removed.
Contract history — v66.1 → v67
Breaking at v67: four output fields removed. v66.1 and v66.2 were additive and are recorded here with it.
Reshaped output — get_agents_hub (v67, breaking)
summarylosesmessagesUsedToday,dailyLimit,messagesUsedPercentandavgCostPerMessageUsd. They reported the in-app arena chat's daily message quota, and that chat is retired, so every value would have read zero. A strict client that reads them fails to parse the summary. The rest of the summary is unchanged; per-agent spend stays on each row'scost24hUsd.
Wider surface — two tools added (v66.2)
scan_coin_agents(coinTicker;mcp:read) — one coin evaluated against every one of the caller's own agents, ranked in four arrays:qualifiedby score,rejectedwith the first failing gate,unscorable, andineligiblewith the codepropose_entry_decisionwould refuse the agent with (AGENT_NOT_ACTIVE,MODEL_INACTIVE,AGENT_HALTED).rankis global across the four. Rate-limited per user: a refusal isRATE_LIMITEDwithretryAfterSeconds.get_onboarding_requirements(no input;mcp:read) — the caller's readiness ladder: every rung, the spine counts and the next rung.
Reshaped output — get_strategy_draft (v66.1)
draftgainslastSource— the surface of the draft's latest write (form,commander,telegramormcp), as the agent and radar draft reads already publish. Before v66.1 every read of an existing strategy draft failed its output check withINTERNAL_ERROR; it now succeeds.
Vocabulary
toolCount goes 132 → 134. The added tools' schemas are new; get_strategy_draft's and
get_agents_hub's output schemas move. Descriptions that named the app's retired Agent Toolbox trade
tab now name Commander.
Contract history — v66
Breaking: one tool retired and one input reshaped. Radar content now commits only as a draft the player was shown a live preview of. The player's radar builder and every conversation share one unsaved draft per coin.
Removed tool — something you call no longer exists
upsert_radar_deploymentis retired, with no alias. Calling it is an unknown-tool error. To deploy or change a coin's Radar policy:stage_radar_deployment_draftthe axes you are changing;preview_radar_resolutionwithrequest: { kind: "DRAFT", draftVersion }at the version staging returned;show the player the resolution, and on their word call
commit_radar_deployment_draftwith thepreviewTokenthat preview returned andconfirm: true.
Pausing is no longer a flag on the write: use
pause_radar_deployment.
Rejected input — something you send is no longer accepted
preview_radar_resolution'srequestneeds akind.{ kind: "SLOTS", deploymentTimeframe, slots, simulatedRegime? }is the old request.{ kind: "DRAFT", draftVersion }previews the player's draft of the coin at that version, composed over the deployed policy exactly as the commit writes it.{ kind: "COMMITTED" }previews the deployed policy a resume would arm. A request withoutkindis refused.
Wider surface — seven tools added
stage_radar_deployment_draft,get_radar_deployment_draft,list_radar_deployment_draftsanddiscard_radar_deployment_draft(mcp:read). The draft axes areDEPLOYMENT_TIMEFRAME,RULES(the complete ordered rule list, first = highest priority) andDEFAULT_SLOT, each written whole. None of them commits or arms anything.commit_radar_deployment_draft(coinId,previewToken,confirm: true;mcp:wager) — the only MCP committer of radar content.pause_radar_deployment(coinId;mcp:wager) — no revision and no certificate: a disarm never waits on a read.resume_radar_deployment(coinId,previewToken,confirm: true;mcp:wager) — needs the certificate a liveCOMMITTEDpreview returned.
Reshaped output — preview_radar_resolution
Gains
previewToken, the certificate a LIVE preview (nosimulatedRegime) ofDRAFTorCOMMITTEDearns — null otherwise, and good for five minutes. It also gainsenabledAfterCommit: whether the policy trades once the previewed write lands. A paused policy's draft stays paused.
Refusals worth knowing before you commit or resume
The certificate is bound to what you previewed: your credential, the player, the coin, the subject, the deployment and its revision, the draft version and the composed content. If the player edits the draft, another commit lands, the coin is redeployed, or an agent in it is deleted, the commit or resume is a
CONFLICTand nothing is written. Preview again; never reuse the old certificate.commit_radar_deployment_drafttakes only a draft certificate, andresume_radar_deploymentonly a committed-policy one.discard_radar_deployment_draftwithconfirm: truerequiresexpectedVersion: the version the unconfirmed call named and the player was shown. A draft that moved since is answered again, and nothing is removed.delete_radar_deploymentalso ends the player's radar draft of the coin.
Vocabulary
toolCount goes 126 → 132. preview_radar_resolution's input and output schemas move, and
upsert_radar_deployment's are removed.
Contract history — v65
Breaking on one input, and four refusals behind unchanged schemas. Agent staging now names the draft version its proposal was read against, and three commits and one stage step aside where they would otherwise land around the player's open draft or on a strategy that moved.
Rejected input — something you send is no longer accepted
stage_agent_draftrequiresdraftVersion, an integer ≥ 0: theversiontheget_agent_draftread you proposed against returned, or0when it returned{ draft: null }. A call that omits it, or sends a negative or fractional number, is refused at the boundary. On a new create draft send0, then reuse theversioneach accepted call returns.
Refusals worth knowing before you stage or commit
A
draftVersionabove the draft's own is refused. A draft's version only grows, so no read ever returned it: the draft you read was saved or discarded since. Read again — never raise the number to get past the refusal.The contested-axis refusal is measured from your
draftVersion, not from a read the call makes for itself, so an edit the player made between your read and your stage is refused by name rather than silently overwritten.update_strategy_signal_ruleandrestore_strategyanswerCONFLICTwhile the player holds a draft of that strategy. Compile the tune and stage it withstage_strategy_plan; for a restore, ask the player to save or discard their draft first. A caller whose player holds no draft is unaffected.stage_strategy_planrefuses a plan the strategy committed past after the plan compiled, with the same answerapply_strategy_plangives: compile again against what is committed now.Every refusal around an open draft names its version in
details.draftVersion— the committers above, the agent committers, and a stage refused for a version above the draft's own — so you can read the draft the player holds and propose against it.
Changed meaning, unchanged shape
A draft axis's
sourcenames the door it came through. A Telegram Commander turn stampstelegramand a connected client stampsmcp, where every staging call used to stampcommander. Both values were already in the enum.A draft's
versionkeeps counting after a save or discard empties it. The next draft for the same agent or strategy starts above the last version rather than at1, so a version you read is never reissued to a different draft.
Vocabulary
toolCount stays 126. One input schema moves, stage_agent_draft's; no output schema moves.
Contract history — v64.1
Purely additive in schema, with one refusal behind an unchanged one. An agent's unsaved configuration now has a server home — the same owner-scoped draft strategies gained in v63.1 — so the agent form, Telegram and a connected client all see one set of unsaved values. These tools are how a conversation reaches it, and the two agent committers now step aside while it is open.
Wider surface — four tools added
stage_agent_draft({ request: { agentId?, axes } })writes proposed axes into the player's draft for an agent and commits nothing: the values become the agent's configuration only when the player saves. Unlikestage_strategy_planit takes the axis values —IDENTITY,BEHAVIOR,MODEL,TRADING_CONFIG, and for an agent not yet createdSTRATEGY_BINDING— each written whole, soBEHAVIORcarries all three of risk, outlook and conviction andTRADING_CONFIGthe complete agent-owned configuration. Axes you omit keep their values. Structure is checked on the way in; ranges are checked only when the player saves.Omit
agentIdto open a create draft. The server mints the id and the response carries it; name that id on every later call so one draft accumulates rather than a second opening beside it.get_agent_draft({ request: { agentId } })reads one draft, or answers{ draft: null }— not part-way through that agent is a value, not an error.baseRevisionisnullfor an agent not yet created, andbaseMovedreports an agent committed past the revision the draft was written against.list_agent_drafts({ request: { cursor? } })answers "what am I part-way through?" for a conversation holding no agent id, newest first. Offer to continue one of these before starting a second.discard_agent_draft({ request: { agentId, confirm } })destroys one draft on the player's word. Called withconfirm: falseit destroys nothing and is refused with when the draft was last written and which surface wrote it — tell the player that, then ask.
Refusals worth knowing before you stage or save
A staged proposal refuses the axes the player typed after your call read the draft. The refusal names the axes; read the draft again and propose against what they now have. Another surface's write to a different axis is retried once for you, then reported as a conflict.
STRATEGY_BINDINGis refused on an agent that exists. Rebinding replaces an agent's configuration and stays its own confirmed call,rebind_intelligence_agent.update_intelligence_agentandrebind_intelligence_agentanswerCONFLICTwhile the player holds a draft for that agent, naming staging as the act available. Nothing about their input or output changed, and a caller whose player holds no draft is unaffected — this is the one change existing code can observe.
Vocabulary
toolCount 122 → 126. No input or output schema of an existing tool moves.
Contract history — v64
Breaking, and it is a removal you will feel on two tools. A vocabulary that never depended on your draft was being re-serialized into every preview result; it now lives on discovery and the preview names it.
Reshaped output — preview_strategy_report
conditionColumnscovers only the sections your report RENDERS. It used to cover every section the server's header inventory holds, which meant every report-level scalar on every preview whether or not its module was placed — 36,652 bytes of payload, with each metric's gloss repeated once per scope (11 market-breadth sentences published as 99). If you readsession-field,market-breadthorreference-pairsgroups off a preview, they are gone.Nothing stopped resolving. The header inventory is unchanged, so a condition naming
mktBreadth_cryptoresolves exactly as it did. What changed is where you read its prose from.vocabularyDigestis new — the sha256 identity of the authoring catalog that render described its columns against, and the same digest a plan token binds asauthoringCatalogDigest. Resolve the vocabulary once, cache it against this string, and re-resolve when it moves.structure.columns[].meaningis gone. It was a positional per-header string that neither serialization ever rendered — the glossary paragraph insection.textis built fromspanFragments, and the structured arm always omittedmeaning.spanFragmentsis unchanged.budgetUsage.estimatedTokensnow measures the whole served payload, not the rendered module text inside it. Existing drafts read several times higher against the same cap. That is the fix, not a regression: the byte cap is measured on the serialized result and cannot be reported inside it, so this is the only meter that can warn you before a refusal — and it was counting about a tenth of what actually refuses.
Wider output — list_strategy_vocabulary
scalarFamiliesis where the vocabulary went. Every report-level scalar family with its section, and one entry per METRIC carrying its gloss, legal condition operators, closed label vocabulary, read contract (closesReadable/developingRead; v84 replacesclosesReadablewithconditionHold) and the scopes it is measured at. One gloss per metric with its scopes named against it, never one per pair.Served whole under every category. A scalar describes the report, not a metric family, so it is not filtered by the
categoryyou asked for. The ambient session family carriesmoduleKey: null— it places no module and its operands are nameable all the same.
Reshaped output — compile_strategy_plan
reviewContext.columnsis gone. It recompiled a full column contract per authored custom column — about 2,100 bytes for a five-header trajectory — describing columns the embeddedreportPreview.conditionColumnsalready describes per rendered header. Askget_strategy_column_contractwhen you want a column's exact normalized contract.approvedPlan.creationSeedis gone. It was the dense 84-rule scorecard before the overrides, published beside apostState.signalRulesthat is the same list after them and anexplicitRuleOverridesnaming exactly what differs. Apply never saw it either.
Wider input acceptance
The section array and a custom section's column array lose their
maxItems: 64. That bound restated a configured cap of 32 as a looser 64 that never refused anything.budgets.sectionsandbudgets.sectionColumnsfrom discovery are the published values, and the server enforces them before it reads any market data.
What to do
Call list_strategy_vocabulary once for the scalar vocabulary, key your cache on the preview's
vocabularyDigest, and drop any code that reads scalar groups off conditionColumns,
structure.columns[].meaning, reviewContext.columns or approvedPlan.creationSeed. A preview's
size now tracks the report and cohort you composed rather than the size of the platform's catalog.
Contract history — v63.2
Purely additive again, and smaller: one optional input field. Drafts now cover a strategy that does not exist yet, so a playbook you author in chat survives between compiles instead of starting over each time.
Wider input — one optional field on one tool
compile_strategy_plan's CREATE arm gains an optionalstrategyId, naming the create draft this compile continues. Omit it and the server mints an id exactly as it always has, so nothing you send today breaks.Send it to accumulate; omit it to start fresh. A first CREATE names nothing and mints; stage that plan and the draft opens at the minted id, which
list_strategy_draftsand the plan itself both report. Name that id on the next CREATE and the second plan carries the same identity, so the one draft accumulates rather than a second opening beside it.WHY YOU HAVE TO SAY IT. The server will not choose for you. A compile knows only who you are — it has no conversation id — and you may hold several create drafts, so picking one could write this playbook's values over another's. A compile that names none mints a third id rather than guessing.
It names a draft; it does not choose an id. It is accepted only as the identity of a create draft you own. An id naming no such draft is refused, as is one naming a draft for a strategy that already exists.
Behaviour changes behind unchanged schemas
stage_strategy_planadmits a CREATE plan, opening the create draft at the plan's own id when none exists yet. Its input is still{ planToken }alone, and the contested-axis refusal applies identically.list_strategy_draftsrows gainbaseRevision. It isnullfor a create draft — a strategy that does not exist has no committed revision to be based on — and that null is the only thing that distinguishes one. There is nokind,isNeworstatusfield beside it.A create draft reserves nothing. It consumes no quota and holds no name, so both are decided when the strategy is actually created. Holding drafts past your limit is legal; creating past it is not.
Vocabulary
axes.create gains strategyId. No domain gains a value, and toolCount stays 122.
Contract history — v63.1
Purely additive: four new tools, nothing you send today changes. A strategy now has an
owner-scoped draft — the unsaved authored values a player is part-way through — and these are how
an agent reads it, writes a compiled plan into it, and destroys one. The canonical record for every
contract move is docs/architecture/MCP_CONTRACT_HISTORY.md in battlegrid-app; the served version
is what the handshake announces.
Wider surface — four tools added
stage_strategy_plan({ request: { planToken } })writes a compiled plan's own changed axes into the owner's draft. It commits nothing and does not spend the plan, so the same token is still directly applicable afterwards.planTokenis the only member the request accepts.get_strategy_draft({ request: { strategyId } })reads one draft, or answers{ draft: null }— "you are not part-way through this one" is a value, not an error. It reportsbaseMovedwhen the strategy has been committed past the revision the draft was written against.list_strategy_drafts({ request: { cursor? } })answers "what am I part-way through?" for a conversation holding no id. It includes a draft whose strategy you can no longer see, markedstrategyExists: false, because that is exactly the work a player has lost track of.discard_strategy_draft({ request: { strategyId, confirm: true } })destroys one draft. The confirmation is required and unsaved values have no other copy; the strategy, its revisions and your other drafts are untouched.
Refusals worth knowing before you stage
A staged plan refuses the axes you typed after it compiled.
stage_strategy_plananswersCONFLICTnamingcontestedAxes, the version the plan compiled over and the draft's own version per axis. Compile again so the plan absorbs those edits, then stage. A previous staging on the same axis is not a contest — only the owner's own hand is.An apply refuses a plan whose draft has moved since. Moved, discarded and already-committed-by-a-sibling are one answer with one recovery: compile again.
Vocabulary
toolCount 118 → 122. No input or output schema of an existing tool moves — the draft version a
plan was compiled over rides inside the opaque plan token, which no schema declares.
Contract history — v61
One part to read first: the per-condition evidence clock is gone, and what replaced it is not a
rename. Which bar a condition reads is now decided by the surface asking — a decision reads completed
strategy bars, a display read shows the forming one — and by each candle column's own Confirmed /
Developing selector. closes survives and changes meaning. The canonical record for every contract
move is docs/architecture/MCP_CONTRACT_HISTORY.md in battlegrid-app; the served version is what
the handshake announces.
Rejected input — something you author is no longer accepted
A condition entry carrying
clockis REFUSED (61.0.0,read-higher-timeframes-per-column). The authoring schemas are.strict(), socompile_strategy_plan,apply_strategy_plan,fork_strategyand the HTTP save alike fail the body with the unknown-key error. There is no replacement key to send: the decision instant belongs to the caller, not to the condition. Two input hashes move,compile_strategy_plan's andpreview_strategy_report's — the two tools that accept a condition entry.closesstays mandatory and means something new. It is now held for N completed strategy bars (1–5). Above1it is legal only over a header a completed bar actually moves, at or above the strategy timeframe, never over a developing read, and only where the condition carries a clause of its own — a referenced condition resolves once and contributes the same answer to every bar, so a hold reached only through a reference would count reads that never happened.
Changed shape — what you receive moves
ConditionOutcome.closeClockbecomeshold, and it is NON-NULL for every condition. A one-close condition reads0 or 1 of 1rather than serving an absence, so a client no longer branches on whether the reading exists. The count is taken from completed strategy bars whatever basis the surface evaluated on.ReportConditionColumnDTO.closeClockReadablebecomesclosesReadable, beside a newdevelopingRead. The first answers whether a condition addressing that header may hold more than one completed bar; the second states whether the header reads the bar still in progress at the decision instant. Both are server-supplied — derive neither. Eight output hashes move, covering every tool that serves a strategy's conditions or a report's addressable columns.
Wider input — nothing you send today breaks
The
barsselector is declared on eleven candle-series transforms, not four.value,classifyZone,classifyState,distance,spread,crossDetectandbandTouchjointrajectory,aggregate,efficiencyandmaxShare. It carries no default value, because the default is a rule rather than a constant: Confirmed ("closed") above the strategy timeframe, and at or below it the series as the frame carries it. The resolved answer is served per column oneffectiveParameters.bars—nullwhere discovery has no anchor to resolve a rung against.A higher-timeframe level is measured from the current strategy-bar price. Distances and the candle label classifiers compare against the frame's current price rather than the higher-timeframe bar's own close, which could be a full higher-timeframe bar stale. A distance chained into a series keeps every slot on its own bar's close.
Vocabulary
domains.conditionClock is removed; domains.columnBars: ["all", "closed"] takes its place, and
axes.condition loses clock. toolCount stays 117.
Contract history — v60
One release train, four numbers, and two parts to read first: propose_entry_decision no longer
decides in the call, and a closed trade's tradeStatus now follows its net P&L. v55 through v59 are not written up here; the canonical record for
every contract move is docs/architecture/MCP_CONTRACT_HISTORY.md in battlegrid-app, and the
served version is what the handshake announces.
Rejected input — something you author is no longer accepted
The entry axis names no bar but the strategy's own, and three of its keys are gone (60.0.0,
decide-entry-on-strategy-close). The authoring schemas are.strict(), socompile_strategy_plan,apply_strategy_plan,fork_strategyandrestore_strategyREFUSE a body carryingentry.confirmTf(the deciding bar is the strategy's own timeframe, so a required input whose only legal value was another field of the same strategy is absent rather than mirrored),entry.closesandentry.bandAtrMultiple(a multi-bar hold is declared on the condition that needs it; the displacement band is replaced by the platform's own entry-deviation gate, measured against the decided close), and the wholeexitobject (the open-position exit lane judges one closed candle of the position's strategy timeframe). Exactly one input hash moves,compile_strategy_plan's.entry.trigger: AT_SIGNALis retired for authoring (60.0.0,retire-at-signal-trigger, riding the same number). Refused on every authoring surface and on a RESTORE, which rebuilds a stored revision through the same value object. The enum member stays READABLE on every strategy read and on a fired decision's provenance, so a pre-retirement revision is still legible — it just cannot be re-authored. The three that remain areON_CANDLE_CLOSE,STOP_THROUGH_LEVELandON_RETEST.
Changed meaning, unchanged shape
tradeStatusfollows NET P&L on every closed trade (60.3.0,label-trade-outcome-by-pnl). No schema hash moves for this and the values you receive change anyway. It used to map the close REASON onto a verdict — everyTAKE_PROFITwasWON, everySTOP_LOSSwasLOST, and aMARKET_CLOSEof either sign was the neutralCLOSED— so a stop that filled after a break-even reprice was reported as a loss and a take-profit eaten by fees as a win.LIQUIDATEDstill outranks the number, because a force-close is not a verdict about the trade;CLOSEDnow means only that there is no outcome row to judge. A client that countedWONrows was counting take-profits.A proposal is QUEUED, not decided (60.2.0,
queue-manual-entry-for-close). This is the entry in this section to act on.propose_entry_decisionregisters a request against the agent's next strategy-bar close and returns immediately: no model runs, nothing is spent, and the call carriestype: "queued"withrequest—requestId, the bar (barStart), when the answer is due (decidesBy) and the last instant that bar may still be decided (windowEndsAt). The answer arrives later, in the agent's conversation and, when it proposes a trade, inlist_pending_approvals. A close that does not qualify, a window that passes with no sweep, and a bar the agent's own radar deployment decided first are each recorded in the conversation instead.recommendationandno_tradeREMAIN in the union: the idempotency registrar replays results recorded before this release for their TTL, so a client that dropped those members would fail on its own retry. A client written against 60.2 handlesqueuedanderror; one that must also replay handles all four.
Reshaped output — the same call returns a different shape
The four retired entry keys leave every strategy read (60.0.0) —
get_strategy,list_strategies,fork_strategyand both plan envelopes — andconfirmTimeframesByMainCandleleaveslist_strategy_vocabulary, because there is no confirm set left to publish.entryDiscipline.closesand.bandAtrMultiplegonumber→number | null(60.0.0) onget_trade_outcome_by_decisionandlist_trade_outcomes. Null on every decision fired after this release, which authors neither; a non-null pair dates the row to the arming era.
Widened enum — new members your own copy rejects
TradeConvErrorCodegainsREQUEST_PENDING(60.2.0), on the SURFACE arm ofpropose_entry_decision's error: one pending request per user and coin, so a second is refused. A coin already carrying a pending or live position is refused before anything is queued, as an ENGINE-originOPEN_POSITION_CONFLICT— a member that was already published, reaching this surface for the first time.
Additive in the same span
The exit names the leg that filled (60.3.0,
label-trade-outcome-by-pnl).TradeOutcomeDTOand the pipeline outcome summary gainexitRepriceSource: the reprice that placed the protection leg which actually closed the position —BREAK_EVEN,TRAILING,TIME_DECAY,MANUAL_OVERRIDE,UPDATED— or null on every close no protection leg filled and on a leg that was never repriced. It is the mechanism behind the verdict above, so read it besidecloseReasonbefore calling a stopped-out trade a failure. The enum is the oneget_position_audit_historyalready publishes. Four output hashes move; no input schema does.Two tools join the catalog for the request lifecycle (60.2.0,
queue-manual-entry-for-close).get_entry_request(read scope) reads a request that is still pending and isNOT_FOUNDonce it has been answered, cancelled or expired — the answer is in the conversation, not there.cancel_entry_request(mcp:wager) withdraws one before its bar is decided.toolCount115 → 117.Three radar reads publish the close decision (60.1.0,
add-radar-close-decision-state).get_radar_deployment,list_radar_deploymentsandpreview_radar_resolutioncarry one further key onresolvesNow:closeDecision, non-null whenever an agent is on duty for the pair. It carries the decidingtimeframe, thenextCloseAtinstant, astateofWAITINGorDEFERRED(a bar has closed and no sweep has decided it yet), andlast— the bar, the instant, the outcome (FIRED/NOT_QUALIFIED/MISSED), the gate and the closed reading's score against its minimum — or null before the pair's first close decision.Read it first on a close-deciding pair. The sibling
qualifiedandqualificationBlockfields are the per-minute DISPLAY reading there and decide nothing, so an agent ranking them reports "qualified — watching for a setup" about a pair whose last three closes were each refused. Neither field is removed or reshaped. ANOT_QUALIFIEDoutcome with a NULL gate beside a score at or above the minimum is the consumed edge — the close qualified and the baseline was already spent — stated by the server so no client compares the two numbers.
Contract history — v37 → v54
Eleven majors reached authors while this section stopped at v36. That gap is the mechanism, not an oversight: since v31 a contract move needs no release here, so nothing forced a note to be written — and the documentation ships inside the tarball, so a note written but unpublished reaches nobody. Both halves are now closed by a rule keyed to the served contract rather than to a release of this package.
50.0.0 and 51.0.0 arrived late, and the reason is worth naming. The re-vendoring errand that
used to carry these notes is now a generated export
(battlegrid-app/server/scripts/export-mcp-skills.mjs), and it owns three paths — skills/,
skills/EXPORT.json, and the vendored digest. It deliberately does not touch this file. So the
digest kept arriving on time while the note stopped travelling with it, and this section sat at
v49.5 against a served contract of 51.0.0. Nothing a client could observe was wrong; what was
missing was the sentence telling them so. A contract move still needs a human-authored entry
here, and the export lane will not remind you.
Accepted again — input that was rejected now compiles
Nothing to migrate. This is the one direction that cannot break a client: a body the server used to refuse is now stored. Listed because a client that special-cased the refusal can delete that branch.
A signal rule patch no longer forces you to restate
allocationandrequired(51.0.0,fix-signal-rule-patch-semantics). Onupdate_strategy_signal_rule, and on theruleselement ofcompile_strategy_plan, both fields become optional and joinparamsunder ONE omission rule: an omitted mutable field preserves the stored value for that signal. "Raise this signal's weight" is now expressible.{ "strategyId": "…", "expectedRevision": 7, "signalId": "volume_surge", "allocation": 3 } // `required` and `params` keep exactly what is storedEvery existing client keeps working — a complete payload is still a valid patch — so this is listed for what you can now STOP sending. Before it, both fields were mandatory on every rule surface, so a caller that had not first read the current rule had to invent a value it was never asked about. That is not hypothetical: revision 5 of a production strategy flipped
requiredfalse → true unasked while moving a weight 2 → 3, turning a scoring signal into a mandatory gate — which changes whether the agent takes trades at all.One boundary on the newly legal ground, and it breaks nothing: a patch carrying no mutable field is refused — "A rule patch must change something: supply at least one of allocation, required or params." — rather than minting a no-op revision. Under 50.0.0 that request could not be formed at all, so nothing that used to work is now refused.
An arming trigger no longer constrains its required conditions' clock (49.4.0,
restore-arming-trigger-authoring).compile_strategy_plan,apply_strategy_planandfork_strategyaccept a strategy whose entry trigger isON_CANDLE_CLOSE,STOP_THROUGH_LEVELorON_RETESTwhile a required condition reads theLIVEclock. v48.1 announced that pairing as rejected, naming the offending condition key onVALIDATION_ERROR; that refusal is gone.Why it was withdrawn, since the reasoning matters more than the rule: it ran against the whole assembled strategy, so it refused every edit to a strategy in that shape — a rename, one report column, one signal weight — plus restore and fork. For a strategy whose required conditions read columns that can never carry a
CLOSEclock (zone distances, perp/spot flow), there was no legal shape to move to at all. And its premise — that the pairing can never fire — was measured before the arming lifecycle was corrected, and no longer holds.The underlying question, should an entry that waits for a close be decided on a forming bar, is now settled where the decision is made rather than by refusing the author's declaration.
Changed meaning, unchanged shape
blocksScanGatereadstruefor a class it did not (50.0.0,own-scan-served-set-once), onpreview_radar_resolution. Blocking is now derived from the lane's served set rather than switched over the reach reason, so aFEED-reason refusal on an operand no reader in the lane serves BLOCKS instead of deferring. No field changes shape, and a client that already renders the key renders the new answer — but a client that treatedblocksScanGate: falseas "this will resolve once data arrives" now sees a deployment that will not fire. Nothing in the payload tells you this moved.Three tools serve different values for identical input (47.3.0,
derive-scan-fetch-from-report). The radar scan leg now derives its timeframe fetch from the strategy's report rather than the on-duty agent's three perception rungs, so a required condition addressing an absolute timeframe outside those rungs — never evaluated at scan before — now is. No field moves; the values do:Tool
What moves
preview_radar_resolutionconditionReach[].reachReasongoesAGENT_TIMEFRAME→nullget_radar_activityscanReachReasonmoves the same way, on new rows only — rows already written keep what they were recorded withget_agent_coin_qualificationreachReasonmoves the same way; its siblingverdictmovesUNMEASURABLE→ a decided verdictRead the last one carefully: a client treating
UNMEASURABLEas "this gate is switched off" will now see that gate BLOCK.AGENT_TIMEFRAMEkeeps its member and narrows to the one cause no fetch can discharge. Nothing in the payload tells you this moved.
Rejected input — something you author is no longer accepted
A condition naming a
swingHi/swingLoheader is refused — the indicator is a Donchian channel (53.0.0,rename-donchian-channel). The rolling-window extremes indicator computed the highest high and lowest low of the trailing 20 closed bars — a Donchian channel — under a swing point's name, and the name asserted a property the value does not have (a swing high survives being broken; a channel edge re-anchors the instant it is). Every layer of the vocabulary moves at once, with no alias: a clause oncompile_strategy_plan,apply_strategy_planorfork_strategynaming a header on the old stems —dist_swingLo,dist_swingHi_4h,dist_swingLo_rank_near, any timeframe- or rank-suffixed form — is refused asCONDITION_COLUMN_UNKNOWNwhere 52.0.0 accepted it. The same shapes exist on thedonchianHi/donchianLostems, whichget_strategy_column_contractlists with the labels 20-bar high / 20-bar low. Every value is the same number under its new name.Rename the stems (
swingHi→donchianHi,swingLo→donchianLo, suffixes unchanged) in every condition and Market Read marker you author. That is the whole migration for what you send.entry.levelSourceis refused — the level is derived, never authored (52.0.0,derive-entry-level). The strictentryobject oncompile_strategy_plan,apply_strategy_planandfork_strategyis six keys —trigger,confirmTf,closes,bandAtrMultiple,levelOffsetAtrMultiple,validForBars— and a body carryinglevelSourceis refused naming the key where 51.0.0 accepted it.STOP_THROUGH_LEVELrests a stop past the swing channel's CURRENT edge in the trade's direction (the 20-bar high for a long, the 20-bar low for a short);ON_RETESTrests a limit in front of the edge a close most recently BROKE. Your two dials are the unsigned distance from that edge (levelOffsetAtrMultiple, 0–2 ATR; a long adds, a short subtracts) and the bar validity (validForBars, 1–24). A meaningfulvalidForBarsunderAT_SIGNALorON_CANDLE_CLOSEis now refused asPARAMETER_NOT_HONOUREDlike the other level dials.Drop the key. That is the whole migration for what you send; the six-key example below is the current shape.
update_strategy_signal_rulerequiresconfirm: truewhen the strategy has bound agents (51.0.0,fix-signal-rule-patch-semantics). The write re-materializes scoring configuration onto every bound agent immediately — including agents holding open USDC positions — and until now nothing on the server asked. A rule edit on a strategy with one or more bound agents is refused without the flag, and the message names the count. An edit on a strategy with nothing bound is unaffected, and so is every path through the web editor.Send
confirm: true. That is the whole migration, andconfirmis published on the input schema — but nothing in the schema says WHEN it becomes mandatory, because the condition is the bound-agent count rather than the shape of your body. The refusal rides the existingVALIDATION_ERRORcode, so a client that omits it discovers the rule at the refusal.Why it moved to the server: the guard existed, but only as served prose the calling model could decline — and did, twice in production on 2026-08-24.
archive_strategyandrebind_intelligence_agenthave taken a server-enforcedconfirmall along; single-rule tuning was the outlier among its own siblings, and it is the one that writes to scoring.A radar deployment is refused when its strategy reads a session-field scalar (50.0.0,
own-scan-served-set-once).upsert_radar_deploymentrefuses a deployment whose slot agents' bound strategy carries a condition reading one of five SESSION-FIELD scalars —fieldPlayers,fieldUpBias,fieldBiasDir,captConc,picksSpread— withCONDITION_OPERAND_UNSERVED_IN_LANE. A body accepted under 49.5.0 is refused under 50.0.0 without one byte of it changing.Why a refusal and not a warning. Those five describe a game SESSION, and radar runs outside a session at BOTH its stages — so such a condition can never resolve there. The deployment formed no fire edge and the agent did nothing on that coin, silently, forever. The refusal converts a permanent silence into an error at the moment you author it.
Migrate by moving the clause to a scalar radar reads — the Market Breadth or Reference Pairs families, which are market-wide reads with no session dimension — or by binding the strategy to an arena agent instead. The error carries both halves:
allowedDomainenumerates every servable header, and the message names the sections.Strategy authoring is untouched by this bump. The same strategy is legal, and reads those scalars correctly, on an arena agent — which is why the refusal is on the DEPLOYMENT and not on
compile_strategy_plan/apply_strategy_plan.A benchmark-bound section no longer accepts crowd metrics or rank transforms (49.0.0,
fix-benchmark-legality-save-path). On a custom section carrying a non-nullbenchmarkTicker, a column whose metric is enrichment-stage (theCROWD_*family,FLOW_ALIGN,SMART_RETAIL,CAPTAIN_CONF,CONFIDENCE,SETTLED_AT, thePERP_SPOT_*trio) is refused withREPORT_COLUMN_BENCHMARK_METRIC_UNSUPPORTED, and one carrying aranktransform in either the direct or the chained position withREPORT_COLUMN_BENCHMARK_TRANSFORM_UNSUPPORTED. Every tool accepting a section array is affected, and no other byte of your body changes.Fix it by moving the column, not by retrying. Both readings are defined relative to the cohort being evaluated — a crowd reading is what this session's players did, a rank is a position among the coins under evaluation — and a benchmark is deliberately outside that cohort. Neither has a value there, which is why such a column could never render. Put it on an ordinary section (
benchmarkTicker: null), or drop it.This is a fix, not a new rule. The restriction shipped with benchmark sections and was already enforced by the column builder, by report materialization, and by
get_strategy_column_contract— so an author who checked a column against discovery first has been seeing this refusal all along. What changed is that the SAVE path now asks the same question. Previously it did not, so such a section persisted and then failed at every evaluation instead of at authoring.A custom report section no longer accepts
timeframe(48.0.0,remove-section-anchor-override). The per-section anchor override is gone. Every tool accepting a section array —compile_strategy_plan,preview_strategy_report,derive_strategy_rule_view, andapply_strategy_plan— refuses a section carrying it:sections[N]: Unrecognized key(s), with no other byte of your body changing. Drop the key. A section's columns resolve against the strategy timeframe, and a column reaches any other timeframe by pinning it on the column (timeframe: { abs: '4h' }) — which it could always do. Relative column references (anchor/lower/regime) are untouched, and are the point: they track the strategy.A custom report section requires
notes(43.0.0,add-authored-section-notes). Every tool accepting a section array —compile_strategy_plan,preview_strategy_report,derive_strategy_rule_view— refuses a section without it:sections[N].notes: Required, with no byte of your body changing. Send explicitnullfor "no note". It is required rather than optional because these payloads are a FULL REPLACE: an omitted key and an explicitnullwould be the same request on the wire, so every rebuild site would silently clear a note its author wrote.benchmarkTickercarries the same required-nullable discipline for the same reason.Every condition requires
clockandcloses(44.0.0,add-condition-clock), andexitarrives with them. A condition entry now carries eight keys, not five.clockis"LIVE"(the previous behaviour — the forming bar) or"CLOSE"(settled bars);closesis how many consecutive closed bars must read true, 1–5, and is always1under LIVE. No wire default, for the same whole-set-replacement reason asnotes: a defaulted key would let an unrelated re-save silently un-clock an enforced money gate back to forming-bar evidence.A
CLOSEclock is accepted only where a closed frame can change the reading — the clause must resolve from the coin's own candle series at offset 0. Frame-inert operands (perp-payload scalars, published rolling changes, ranks, zone entities, regime labels, enrichment metrics, session scalars) are refused withCONDITION_CLOCK_OPERAND_ILLEGALnaming the header and the remedy: split that clause into its own LIVE condition andconditionRefit.exit: trueis legal only underclock: "CLOSE"— an exit fired on a forming bar is an intrabar exit.A strategy requires a six-key
entryobject (44.0.0 for four keys, 47.0.0 for three more —add-entry-on-close,add-level-trigger-execution— and 52.0.0 removedlevelSource,derive-entry-level). Required on every CREATE, oncompile_strategy_plan,apply_strategy_planandupdate_intelligence_agent. A client sending 44.0.0's four-key object is refused withentry.levelOffsetAtrMultiple: Requiredwithout one byte of it changing."entry": { "trigger": "AT_SIGNAL", // | ON_CANDLE_CLOSE | STOP_THROUGH_LEVEL | ON_RETEST "confirmTf": "4h", // the strategy timeframe or the rung below it — nothing else "closes": 1, // 1–5; must be 1 unless ON_CANDLE_CLOSE "bandAtrMultiple": 1.0, // > 0, and <= the platform's entry-deviation gate "levelOffsetAtrMultiple": 0, // 0–2, UNSIGNED distance from the derived edge; 0 unless a level trigger "validForBars": 4 // 1–24 of the strategy's own bars; 4 unless a level trigger }AT_SIGNALwith those values is byte-identical to pre-44 behaviour. The legality matrix runs one way: all six keys are always present, so a MEANINGFUL value under a trigger that ignores it is refused rather than accepted-and-dropped — a dial never silently does nothing.A
strategyTimeframethe platform does not ingest is refused (39.0.0,move-renderer-to-rendered-section) onget_coin_market_context, where it was previously accepted. The number is the only signal a client gets.eventTypegainsENTRY_EXPIRED_UNCONFIRMED(48.1.0,fix-entry-arming-lifecycle) onget_radar_activityandget_radar_activity_summary— an armed entry that reached its episode lifetime without ever receiving a confirming close. A client holding its own closed copy of that enum rejects the new member; one that switches exhaustively on it needs the branch.entryVoidCauseis deliberately unchanged and still carries exactlyBAND,CONDITIONS,STRUCTURAL. Each of those is a measurement taken at a close, so an episode that reached no close gets its own event type rather than a fourth cause —ENTRY_VOIDEDmeans "called off at the close", a claim this outcome must not make.The authoring boundary also gains a refusal with no schema change: a strategy declaring an arming trigger whose
requiredconditions read theLIVEclock is rejected on the existingVALIDATION_ERRORcode, naming the offending condition key. Same code, same shape, new reason — so nothing in the published schema tells you it can now happen.
Removed — no alias exists
SWING_LOW/SWING_HIGHleave the stop and take-profit method enums, and the four S/R indicator keys leave the signal vocabulary (53.0.0,rename-donchian-channel).DONCHIAN_LOWER/DONCHIAN_UPPERanddonchian_upper/donchian_lower/prev_donchian_upper/prev_donchian_lowercarry the same values; nothing answers to the old names.get_coin_market_contextis REMOVED (40.0.0,retire-get-coin-market-context). Calling it returns an unknown-tool error. There is deliberately no alias: a silent redirect would hide a payload shape change from a client that never asked for one. Useget_market_context.get_macd_heatmapleaves the published surface (41.0.0). Same shape of break, same absence of an alias.isPrimaryis removed from every publishedEvaluatedSignal(38.0.0) —get_signal_log,get_public_agent_signal_log_detail, and every other tool returning a signal scorecard. Reading it now finds the key absent rather than false.
Reshaped output — the same call returns a different shape
scan_agent_coinsreturns the ranking, not an explanation of every coin (54.0.0,fix-mcp-scan-row-altitude). The scan used to wrap the app's full per-coin qualification verdict in every row — two directions with candidate-level construction and a stop-loss policy band, four gates each with its own measurement, condition reach reasons, the ATR corridor. For a 78-coin catalog that was 82,147 characters, ~1,053 per row, which is past the tool-result cap of every client we know of: the calling model received a file path instead of an answer, so the tool did not deliver its result even when the scan succeeded.rowsis gone. Three ranked arrays replace it, andrankis global across all three, so reading them in this order reproduces the server's own sequence:qualified[] { rank, coinTicker, scorePercent, coinDataStopped } rejected[] { rank, coinTicker, scorePercent, firstFailReason, scoreShortfallPercent } unscorable[] { rank, coinTicker, coinDataStopped }Array membership now carries what the row
kinddiscriminator and thequalifiesflag used to, and both are gone with them;firstFailReasonis non-nullable on a rejected row, because a non-qualifying verdict always names the gate that blocked it. Rows also loselong,short,gates,tradeableAtrRange,evaluatedAt,coinName,assetClassandcategory.Two things are new. The agent's
agentId,agentName,strategyTimeframeandminScorePercentmove to anagentobject carried once instead of on all 78 rows —nullwhen the scan scored nothing at all. AndscoreShortfallPercentis server-computed: how far below the minimum the score fell, non-null exactly when the aggregate score is what blocked, so you never subtract a published threshold from a published reading yourself.scanStartedAt,coinsScannedandqualifiedCountare unchanged, and so is every coin: same rows, same order, same ranks, same verdicts, same rate buckets, same evaluator. Only the fields moved — about 8.8 KB for the same 78-coin scan.Migration. Read
qualified/rejected/unscorableinstead ofrows, and take the agent's thresholds fromagentrather than from the first row. For a shortlisted coin's full per-direction and per-gate detail, callget_agent_coin_qualificationon up to 12 tickers — it carries every dropped field and costs no second scan (the scan is rate-limited to 3 per agent and 10 per user a minute; the probe is not).Report headers, glosses and signal indicator keys are renamed for the Donchian channel (53.0.0,
rename-donchian-channel). Every report surface —preview_strategy_report,get_strategy_section_template, the agent prompt previews — rendersdonchianHi/donchianLoand theirdist_…/…_rank_nearforms where it renderedswingHi/swingLo, with the labels 20-bar high / 20-bar low and glosses that say what the number is (the highest high / lowest low of the last 20 closed bars — the channel's edges). Signal definitions (get_strategy_signal_definition,list_strategy_signals) and signal-logindicatorValuescarrydonchian_upper/donchian_lower/prev_donchian_upper/prev_donchian_lowerfor the four S/R signals. Signal ids, theSUPPORT_RESISTANCEmodule and its display names are unchanged.Read the new keys. A reader keyed on
swing_high/swing_lowfinds nothing; the values are the same numbers under the new keys.The stored entry discipline no longer names a level source (52.0.0,
derive-entry-level).StrategyDTO.entry(get_strategy,list_strategies, thefork_strategy/archive_strategy/restore_strategyenvelopes) and the apply envelope'spostState.entryloselevelSource.TradeOutcomeDTO.entryDiscipline(get_trade_outcome_by_decision,list_trade_outcomes) loses it and gainslevelOffsetAtrMultiple: number | null— the offset in force at the fire,nullon rows written before its column existed. A reader that rendered the level source renders the geometry fromtriggerand the outcome'sdirectioninstead: which edge and which order shape are a pure function of those two fields, so no stored copy is served.update_strategy_signal_rulegains its own response envelope (51.0.0,fix-signal-rule-patch-semantics). It no longer shares{ strategy }with its siblings. The response is{ strategy, ruleChanges }, whereruleChangesis the server's own before/after pair for the edited signal —[{ signalId, before, after }], each side a full rule object. It isnullwhen the mutation changed no rule, never[].Report the change from that pair, not from memory. The planner always computed the diff and the tool discarded it, so a caller narrating what it just did had only its own recollection of the before-value. One production edit shipped a wrong receipt on top of a wrong write that way, and the write was unreconstructable from the audit trail afterwards.
Additive, but published on a
.strict()shape — a decoder pinned to the old two-key object rejects the new key.fork_strategy,archive_strategyandrestore_strategykeep the sharedStrategyResponseSchemaand publish exactly what they did; it was deliberately NOT widened for them, so this reshape reaches one tool only.An entry void now names the gate that refused it (49.5.0,
fix-arming-trigger-clock-authority), onget_radar_activity_summary. In the cause rollup, theENTRY_VOIDgroup'sgateCodewidens from always-nulltoQualificationGateCode | null: a conditions-side void carries the gate that blocked —AGGREGATE_BELOW_MIN,REQUIRED_COUNT_BELOW_MIN,REQUIRED_CONDITION_FALSE— while a band void staysnull, because that void happens on a reading that qualified and has no failing gate to name.A client that renders the field through the same enum the response already uses on four other cause arms needs no change. One that treated it as a literal
null— a strict decoder pinning the type, or a branch keyed to its absence — sees a value it did not expect. That is the whole migration.Why it moved: the group previously collapsed every conditions-side void under one label. The first 26 in production carried that label while two different gates had produced them, and none of them was a required condition being false. The rollup ships counts rather than rows, so the gate could not be recovered client-side.
The normalized report section loses
timeframe(48.0.0,remove-section-anchor-override), on every tool that publishes a strategy:get_strategy,fork_strategy,archive_strategy,restore_strategy,update_strategy_signal_rule,apply_strategy_plan, andcompile_strategy_plan's post-state. A strict parser rejects the shorter object; a lenient one reads a section whose anchor is the strategy timeframe, which it now always is.get_strategy_column_contractrenames its anchor, both ways (48.0.0). The request fieldsectionTimeframebecomesanchorTimeframe— same meaning, and still optional. On the response,timeframe.requiresSectionTimeframebecomesrequiresAnchorTimeframe, andtimeframe.sectionTimeframeOverrideAllowedis removed: it published whether a column could go in an anchor-overridden section, and no section can be overridden. One call also stops being refused — a timeframe-inert metric supplied with an anchor now compiles.REPORT_COLUMN_SECTION_TIMEFRAME_UNSUPPORTEDleaves theauthoringCodevocabulary.RenderedSection.notesstops carrying provenance (42.0.0,separate-section-facts-from-read), onpreview_strategy_report,compile_strategy_planandget_market_context. A new REQUIREDprovenance: string[]carries it instead. A client reading provenance out ofnotesnow reads an author's prose, or nothing — which is a silent misread, not an error.preview_strategy_report.renderedSections[]gainsauthoredNote(43.0.0), the author's read for a custom row andnullon a platform row. Additive, but published on a.strict()shape.get_radar_activityserves its evaluation curve on the FIRST PAGE ONLY (37.0.0), andget_radar_activity_summaryis added. A client reading the curve off a later page finds it absent.
Widened enum — new members your own copy rejects
The stop and take-profit method enums gain
DONCHIAN_LOWER/DONCHIAN_UPPER(53.0.0,rename-donchian-channel), replacingSWING_LOW/SWING_HIGHon the signal-pipeline detail schemas — trade-setup options, R:R-rejected pairs and candidate levels. A copy of either enum that rejects unknown members must add the two new ones; the two old ones never appear again.TradeExecutionFailureReasongainsLEVEL_NOT_RESTABLE(52.0.0,derive-entry-level): a level entry refused at placement because its resting price sat on the wrong side of the exchange mid — a buy stop at or below it, a buy limit at or above it, and the mirror for a sell. It appears on thelist_signal_logsfailure-reason filter input and on the signal-pipeline execution summary'sfailureReason, with originCLIENT_GATE. There is no distance limit: a level far from the mark rests until its bar validity expires.The authorable metric vocabulary widens by 29 keys (46.1.0,
add-indicator-catalog-coverage): Keltner (KC_UPPER/KC_MID/KC_LOWER), Supertrend (ST_LINE/ST_DIR), Hull (HMA20), WaveTrend (WT1/WT2), QQE (QQE_RSI_MA/QQE_STOP), Parabolic SAR (PSAR), Ichimoku (ICHI_CONV/ICHI_BASE/ICHI_SPAN_A/ICHI_SPAN_B/ICHI_LAG), Williams %R (WILLR14), Stochastic RSI (STOCH_RSI14), session pivots (PIVOT_P/PIVOT_R1–R3/PIVOT_S1–S3), plus four already-published fields that became addressable:BB_UPPER,BB_LOWER,DI_PLUS,DI_MINUS.Additive on the wire — every request you can send today is still accepted. It is called out here because a client holding its own closed copy of the metric enum rejects the new members, and because an agent holding a cached belief that these are inexpressible will substitute for a primitive the platform now serves. That failure raises no error at all: the author is simply told a strategy cannot be built.
EntryTriggergainsSTOP_THROUGH_LEVELandON_RETEST, andEntryLevelSourceis published for the first time with four members (47.0.0). Additive on their own; the required keys above are what make that bump a major.Each signal-checklist item's
measuredobject gains a requiredtriggeredboolean (47.2.0,fix-entry-prompt-signal-evidence) onget_signal_logandget_public_agent_signal_log_detail, on thenumericandcategoricalarms. Additive and MINOR — a client ignoring it is unaffected — but it is named here because a client parsing that output strictly rejects the new key, and because leaving 47.2 unlisted would make a reader wonder what happened to it.The
unavailablearm deliberately does not carrytriggered: "the claim could not be joined to a stored result" and "the signal did not fire" are different states, and leaving the key off that arm makes conflating them a type error rather than a convention.
Additive in the same span
Two tools join the catalog for the agent trade flow (52.1.0,
add-mcp-agent-trade-flow).scan_agent_coinsevaluates every active coin against one of your agents in a single call and returns them server-ranked, on the same use case, buckets and ranking the app's own TRADE-tab scan serves.propose_entry_decisionruns the conversational trade turn headlessly for one (agent, coin) and returns its terminal in the stream's own vocabulary:type: recommendationcarrying the PROPOSEDTradingEntryDecisionDTOrow thatget_entry_decisionandlist_pending_approvalsalready publish,type: no_trade, ortype: errorcarrying the turn'sTradeConvErrorverbatim. Nothing narrows and no existing schema hash moves; the 12-tickerget_agent_coin_qualificationstays as the spot-check probe.idempotencyKeyis REQUIRED onpropose_entry_decision— it is the turn's own key, and a same-key retry replays the recorded terminal rather than paying for a second inference. That includes a post-billingLLM_FAILURE, which is returned as a value for precisely that reason. Pre-engine faults are typed errors instead:RATE_LIMITED,NOT_FOUND,CONFLICTfor a same-key call still in flight, andSERVICE_UNAVAILABLEwhen the surface is switched off. Both tools refuse withRATE_LIMITEDcarryingretryAfterSecondsunder the same per-(user, agent) limit the app itself enforces, so a scan is never served stale or partial.get_regime_snapshotpublishes the evidence behind the verdict (47.1.0,publish-regime-classification-evidence). The snapshot gainsevidence: the quantities the classifier read, the gates it tested them against, the signed margin to the gate deciding whether the current label survives, and the two decision facts only the classifier holds —gateState(cleared/held/dropped) anddirectionSource(di/ema). Nothing narrows; a client that ignores the field is unaffected.Read
gateStatebefore you trust a trend label:heldmeans the ADX hysteresis buffer is carrying the PREVIOUS bar's label rather than this bar re-confirming it — a materially weaker claim wearing the same word, and one no client could previously detect.directionSource: 'ema'is the same shape of warning: the direction came from the fallback that fires precisely when the DI spread is indecisive. The margin is signed so positive always means "the current label survives by this much", in every gate state, so it is safe to branch on its sign.convictionis a BRANCH DISCRIMINATOR, not a confidence: it encodes which rule in the priority ladder matched, not how comfortably it matched. The margins carry comfort. A client reading conviction as a strength score is reading it wrong, and always was — this release just makes the alternative available.Thirteen metric keys join the catalog (47.1.0) — the
regimefamily gainsREGIME_STATE,REGIME_CONVICTION,REGIME_RUN_BARS,REGIME_TREND_GATE,REGIME_TREND_MARGIN,REGIME_TREND_SOURCE,REGIME_DI_SPREAD,REGIME_VOL_ATR_RATIO,REGIME_VOL_BBW_RATIO,REGIME_MOM_BULL_VOTES,REGIME_MOM_BEAR_VOTES,REGIME_CRASH_MARGINandREGIME_CRASH_LATCH, making the composite regime and its evidence addressable in a report column or condition for the first time. Only a client that switches exhaustively onMetricKeyneeds new branches.Not a contract change, but worth knowing if you author conditions: the report grammar's regime metrics now resolve from the confirmed close on every path. They previously resolved from the forming bar when a report was rendered and the confirmed close when the scan swept, so the same condition could read differently in preview than in production. Same wire shape; same bar everywhere now.
Contract history — v12 → v36
The number in this heading is a CONTRACT version, not this package's version. The npm badge at the top tracks the proxy's own code; this section tracks the server's wire contract. They move independently by design: a contract move needs no release here, because a connected proxy relays the contract out of the upstream handshake rather than declaring it. So a package on
31.xlisting contract history up to36.xis correct — not a version someone forgot to bump. Read the live pair from the startup stderr lines orGET /mcp/version; see Rediscovery & versioning for why.
These are the server contract breaks between contract 12 and contract 36. Most of the span shipped while the package sat at 11.0.0; contract 31 landed after this package reached 31.0.0, and the two numbers matching is coincidence — since v31 the announced contract is relayed from the server, so a package version says nothing about a contract version. They are contract history, not package releases: from v31 the announced contract is relayed live and a contract move is no longer a release here. Grouped by what a client observes, with the contract version that introduced each.
The proxy itself is unchanged. It embeds no schemas, pins no contract version, and forwards { request } verbatim. Every break below lands on whatever authors the payload or reads the result, never on the proxy.
Changed meaning, unchanged shape — the one to read first
compile_strategy_planis no longer read-only or idempotent (33.0.0,rehydrate-approved-plan-on-apply). Its input, its output and its behaviour toward your strategy are unchanged — it still changes no strategy, agent or revision — but it now parks the plan it approved for its own apply to read, and each call mints a distinct record and a distinct token.readOnlyHintandidempotentHintare published asfalseaccordingly. A client that retried a compile that had already succeeded, or fanned two out in parallel for one edit, was doing so on the strength of the old annotation: do neither. Compile once per reviewed payload. Nothing in the payload tells you this moved.Position-size presets are now a RISK BUDGET (30.0.0,
split-stop-geometry-from-risk).smallPct/mediumPct/largePctstop denoting a share of the ORDER (notional = pct / 100 × headroom × leverage) and start denoting the share of headroom placed at risk (notional = headroom × riskPct / stopDistancePct, capped at the margin headroom can post). Same keys, same types, same accepted range: nothing in the payload tells you the meaning moved. A client still sending22.0for MEDIUM is asking to risk 22% of its budget on one trade rather than roughly 2%. Typical risk budgets are0.5–3; the platform defaults moved to1 / 2 / 3. Leverage stops multiplying order size and becomes a constraint only.
Rejected input — something you author is no longer accepted
upsert_deployment_policyrequiresenabled(35.0.0,add-arena-deployment-pause). The arena deployment gains an owner-owned pause, and the flag that carries it is required, not optional — a body accepted under contract 34 is refused under 35 without one byte of it changing. Required is the whole point: this call replaces the entire policy, so an omitted key and an explicittruewould be the same request on the wire, and every client that rebuilt a policy without the flag would silently resume a deployment its owner had paused. Read the value fromget_deployment_policyand send it back. There is no separate pause verb — pausing and resuming are this same call with the flag flipped.Do not reach for
enabled: falseto un-deploy. It keeps every slot and stops play, which is the opposite of withdrawing:delete_deployment_policyis the withdrawal verb, and it discards the rules permanently.upsert_deployment_policyrefuses an empty slot set, and its rejection names both routes.A signal rule flagged
requiredat allocation Off is rejected (34.0.0,enforce-required-allocation-invariant).requiredandallocationare two independently editable fields encoding one thing — whether and how a signal participates — so{ required: true, allocation: 0 }was representable and meant nothing: the scorecard's triggered set already excludes Off, so such a rule could neither satisfyminRequiredCountnor block a trade. It is now refused on every rule-writing surface —compile_strategy_plan,apply_strategy_planandupdate_strategy_signal_rulealike.Three things make this one easy to trip over. It is an input-acceptance narrowing: a payload accepted under contract 33 is refused under 34 without one byte of it changing. It is invisible in the published schema —
zod-to-json-schemadrops effects by construction — so you cannot pre-validate it fromtools/list, and the typed error IS the contract: readdetails.inertRequiredSignalIds, which carries the complete sorted list of offending signals, rather than the message, which names a bounded prefix. And nothing is repaired for you: the server will not raise the allocation (that would invent a scoring weight you never chose) nor clear the flag (that would discard your intent silently). Pick one and resend; either satisfies the boundary.apply_strategy_planno longer accepts the plan (33.0.0,rehydrate-approved-plan-on-apply). Its input narrows to{ planToken, confirm }. Aplanmember is rejected as an unknown key — not accepted, not ignored, and with no transitional dual shape — so every client that built the payload breaks on the next connection, which before this is what every published surface told it to do. The server keeps the plan its own compile approved and reads it back, so you copy nothing out of the compile response: forwardplanTokenbyte-for-byte and confirm.Three consequences worth knowing. The 256,000-byte cap on the apply payload is gone with the payload, so a large authored surface no longer becomes impossible to apply through a conversational client; the compiled plan is still capped and compile still enforces it.
PLAN_APPROVAL_NOT_FOUNDjoins the error vocabulary for a token no approved plan answers to — already applied, lapsed, or never issued, all one code, because the recovery is the same in each case: compile again. And a validation refusal (a quota, a name collision, a bound agent that changed, a moved catalog) now leaves the approved plan applicable — clear the cause and confirm again with the same token while it lives, rather than recompiling.The agent brain is no longer a preset union (32.0.0,
remove-agent-presets).create_intelligence_agentstops taking thebraindiscriminated union:modelIdandbehaviorbecome required top-level fields and the{ kind: 'PRESET' | 'CUSTOM' }wrapper is gone, so the old shape fails on the unknown key and on two now-missing required fields.update_intelligence_agentdropsbrainPreset;modelIdandbehaviorstay independently optional and are now always honoured, closing an accept-and-ignore where a named preset silently discarded a model sent beside it.apply_strategy_plannow publishes the same bounds ascompile_strategy_plan(31.0.0, resolving #4495). Eleven position-management dials were declared three times server-side and two copies had drifted, so apply advertisedtrailingGivebackPctas a bare number where compile advertised 25–55, and droppedtrailingTriggerR'smultipleOf 0.01— the constraint pinning storage precision so a sub-precision value is rejected rather than rounded onto the trail-from-entry sentinel0. Nothing bad could ever commit (the digest would not match), but the refusal you got was a binding mismatch, which arrived as a bareINTERNAL_ERROR. Apply's published bounds only narrow to compile's; a client that copies values fromapprovedPlan.postState, as it should, is unaffected. Separately the triominStopLossAtrMultiple/maxStopLossAtrMultiple/minRiskRewardRatiois published as a bare declaration by both tools, its real bounds being runtime-tunable and inexpressible in JSON Schema.The stop-loss ceiling changed unit (30.0.0).
maxStopLossPct(a percent of entry) becomesmaxStopLossAtrMultiple(a multiple of ATR), and the accepted range narrows from(0, 100]to(0, 3]. It is a rename and a re-denomination — mapping the old value onto the new key sends a number one to two orders of magnitude too large. The objects are.strict(), so a 29.x client sendingmaxStopLossPctis rejected with an unknown-key error. A new cross-field rule comes with it:minStopLossAtrMultiple < maxStopLossAtrMultipleis now a real comparison and is enforced.The grid-confidence and trade-conviction bars left the agent (28.0.0,
remove-agent-rule-defaults).tradingConfig.gridMinConfidenceandminTradeConvictionare removed from the shared.strict()config; a bar is declared on the arena slot or radar slot that fires.The agent no longer carries either entry guard (26.0.0).
create_agentandupdate_agentstop acceptingtradingConfig.signalTimeoutMinutesandtradingConfig.maxEntryDeviationAtrMultiple. The schema is.strict(), so a client still sending either is rejected, not silently ignored. Neither has a replacement key on any surface: unlike the strategy-owned fields below, these have no owning surface at all beneath the platform. Oneplatform_configvalue governs the entry-price drift budget for every decision, read at evaluation time; one governs how long an entry may stay unfilled, snapshotted onto the position at creation. Remove both keys and send nothing in their place.The arena stopped granting trade authority (25.0.0,
remove-arena-trade-permissions).upsert_deployment_policyandpreview_deployment_resolutionstop acceptingtradingEnabled,minConvictionandcoinRules[]on a slot.The agent no longer carries an exit policy (24.0.0).
create_agentandupdate_agentstop acceptingtradingConfig.positionManagement. The schema is.strict(), so a client still sending it is rejected, not silently ignored. The twelve dials that decide how a stop MOVES after entry — break-even arming, trailing engagement and giveback, time-decay tightening — are denominated in the setup's own payoff shape (multiples of the trade's initial risk, fractions of take-profit distance, minutes since entry) and read no balance, leverage or exposure, so they belong to the thesis rather than to the account running it. Author them on the strategy instead, throughcompile_strategy_plan/apply_strategy_plan: the post-state gains the same twelve keys beside the trade-level trio, and the plan diff gains apositionManagementaxis. An agent inherits the policy from the strategy it binds. The umbrellaenabledflag is deleted rather than moved — each mechanism's toggle is now the whole truth for that mechanism, so "trailing on, management off" is no longer expressible, a state the server's own monitor and boot recovery already disagreed about.The agent-level trading mode is gone (23.0.0).
create_agentandupdate_agentstop acceptingtradingConfig.tradingMode. The schema is.strict(), so a client still sending it is rejected, not silently ignored. Trading on/off is now scoped per deployment — a radar policy'senabled, an arena slot'stradingEnabled, a per-cointradeEnabled— and approval-before- execution is the conversational surface's own contract, so the account-level switch that sat above both is removed rather than renamed. To stop an agent trading, turn its deployment off (or halt the agent); to make one trade autonomously, arm a radar coin or switch trading on for an arena slot. A newly authored arena slot now starts with trading off.Trailing gained a required threshold (22.0.0,
add-trailing-trigger-r).positionManagementgainstrailingTriggerRas REQUIRED (0–2.0,0.01precision,0= trail from entry); the object is.strict()all-required, so sendingpositionManagementwithout it is rejected.The strategy regime timeframe became derived (19.0.0,
remove-strategy-regime-override). It stops being an authored axis anywhere on the contract and is served read-only.Conditions gained a required
required(16.0.0,add-condition-enforcement-gate). A condition entry omitting the boolean is REJECTED rather than defaulted.The trade-level policy moved to the strategy (15.0.0,
move-trade-level-policy-to-strategy). It leaves the agent authoring surface and joins the setup gates on the strategy.The agent's ATR timeframe axis is gone (14.0.0,
remove-agent-atr-timeframe-axis). ATR is sampled on the strategy timeframe, always.Radar's wall-clock condition changed shape (12.0.0,
unify-deployment-hours-as-sets).
Moved or reshaped output — a field you read is somewhere else
The signal scorecard stops serializing its entries three times over (36.0.0,
mcp-signal-log-contents).get_signal_logandget_public_agent_signal_log_detaildropscorecard.triggeredSignals,scorecard.primarySignalsandscorecard.supportingSignals. Every one held the same entry objectsallEvaluatedSignalsalready carried, so each is one filter over flags every entry still publishes:Removed
Read instead
triggeredSignalsallEvaluatedSignals.filter(s => s.triggered)primarySignalsallEvaluatedSignals.filter(s => s.triggered && s.isPrimary)supportingSignalsallEvaluatedSignals.filter(s => s.triggered && !s.isPrimary)Keep the
triggeredhalf of those last two predicates. Both collections were triggered-only by construction, so filtering onisPrimaryalone surfaces signals that never fired — a silent widening, not an error. No field is removed from an entry: the key set on anallEvaluatedSignalsmember is unchanged, every evaluated signal is still returned whether or not it triggered, anddetailsprose andindicatorValuesare intact. There is no opt-in to get the three back and no default filter. Breaking only if you read one of the three names; on an 84-signal / 16-triggered log the duplication was 10,682 bytes, 28% of the scorecard, carrying no information.The fleet roll-up on
list_deployment_policiesdropsunconfiguredand gainspaused(35.0.0,add-arena-deployment-pause,fix-arena-deployment-undeploy).unconfiguredcounted a deployment holding zero slots — a state that can no longer exist, because a stored policy now carries at least one slot and the withdrawn state is the absence of a policy rather than an empty one. The bucket was constant0at the moment of removal, so no number you read was wrong; a client reading the key still breaks on it, which is why this is a break and not a cleanup.pausedis the owner's own switch, counted separately fromretired— an administrator disabling the arena — because conflating them tells an owner to wait for something that will not happen. Every policy lands in exactly one bucket, so the buckets sum toarenas: worth asserting if you reconcile these counts.AdminApprovedModelDTO.isActivebecamelifecycle(32.0.0,remove-agent-presets) —AVAILABLE/DEPRECATED/RETIRED. The boolean conflated "offered in the picker" with "bound agents may run", so there was no way to stop offering a model without hard-blocking every agent already on it. A client switching onisActivemust switch onlifecycle, and must not treatDEPRECATEDas blocked: that is the state which keeps bound agents running. The agent read DTO dropsbrainPresetin the same move — the marker recorded which named bundle an owner clicked, never a value the runtime read, and the model and soul it stamped are unchanged on every agent.approvedPlanis one object, not an operation union (31.0.0, resolving #4495). It was published as a discriminated union whose discriminator does not survive JSON-Schema conversion, so what actually shipped was a bareanyOf: validating a failing compile response gave you every arm's errors with empty instance paths, and the top one typically complained that an UPDATE was missingcreationSeed— a CREATE-only key — while the field that really failed went unnamed. It is now one object with a literaloperationdiscriminator, andcreationSeed,expectedRevisionandbindingImpactare required and nullable on every operation: a CREATE plan carries a seed andexpectedRevision: null, an UPDATE/RESTORE plan the reverse. If you narrowed on the union arms, readoperationinstead and expect explicitnulls rather than absent keys. If you read those fields without narrowing, nothing changes except that they may now be null.get_radar_activitygained anEDGE_REARMvariant (29.0.0,add-radar-anchor-rearm), and every member gained fiverearm*margin keys plus arearmReasonsdiscriminator. Breaking on both counts if you parse the union strictly.get_trading_config_catalogdrops four trade-default seeds (27.0.0) —defaultMinAtrPct,defaultMinStopLossPct,defaultMaxStopLossPctanddefaultMinRiskRewardRatioleavedefaults. Each seeded a per-agent field that is now strategy-owned: the shared.strict()TradingConfigSchemaalready rejected all four as unknown keys, so the catalog was advertising defaults no request could apply. A client that wants the stop-loss band, the ATR floor or the risk-reward minimum reads them from the bound strategy, which owns and materializes them. Nothing is added in their place.defaultMaxEntryDeviationAtrMultipleanddefaultTtlMinutesare untouched — those are not seeds but the platform values that govern.get_radar_activitygainedblockReasonCodeon every member (21.0.0,fix-block-reason-attribution) — non-null only onBLOCKED_BEFORE_EVALUATIONrows written after 2026-08-17.signal_pipeline's decision became a discriminated union (20.0.0,add-decision-skip-attribution).ENTER/GATEDcarry the seven level fields as REQUIRED;SKIPomits them entirely rather than sending nulls, so readingentryPricewithout narrowing the verdict finds the key absent.get_radar_activitygained anEVALUATION_OUTCOMEmember (18.0.0,add-radar-fire-outcome-journal), and every existing member gainedevaluationOutcome+screenReason.Scalar families became placeable modules (13.0.0,
add-scalar-family-modules); six opt-in scalar headers moved off the sharedsession-fieldsection key.Both entry guards leave every agent-returning shape (26.0.0) — the read side of the input removal above.
AgentTradingConfigDTOdropssignalTimeoutMinutesandmaxEntryDeviationAtrMultipleon every tool that serves an agent, and the explorer trading spec and the agent-review payload drop them too.get_trading_config_catalogdropsdefaultSignalTimeoutMinutesfrom its defaults and theminimumMaxEntryDeviationAtrMultiple/maximumMaxEntryDeviationAtrMultiplepair from its bounds — a bound pair that constrained a per-agent field which no longer exists.defaultMaxEntryDeviationAtrMultipleanddefaultTtlMinutesstay, and are now the values that actually govern rather than seeds a new agent copies. A client reading these objects strictly must drop the removed keys.positionManagementleaves every agent-returning shape (24.0.0) — the read side of the input removal above.AgentTradingConfigDTOdrops the nested block on every tool that serves an agent, and the explorer trading spec drops it too. A client reading these objects strictly must drop the key; one that wants the policy reads it from the bound strategy.get_trading_config_catalogdropspositionManagementPresets(24.0.0) — the pistol ladder (COLT / WEBLEY / BERETTA / LUGER / WALTHER) is retired, not renamed. Once the values live on the strategy, the strategy IS the named bundle, with its own name, description and revision history; a parallel vocabulary of anonymous bundles beside it would be a second name for the same thing. There is no replacement enum to migrate to — list strategies instead. The catalog'sdefaultPositionMgmt*trading defaults go with it, for the same reason: nothing seeds an agent's exit policy any more.tradingModeleaves every agent-returning shape (23.0.0) — the read side of the input removal above.AgentTradingConfigDTOdrops it on every tool that serves an agent, and so do the agents-hub permission envelope, the explorer entry, and both public-profile shapes. A client reading these objects strictly must drop the key.DeploymentResolvedResolutionDTOdropsagentTradingMode(23.0.0) — the field 10.0.0 added, now unnecessary: with no account layer to overlay, the resolvedtradingEnabledis the whole answer about whether the previewed deployment trades.
Widened enum — new members your own copy rejects
DeploymentResolutionStatusgainsPAUSED(35.0.0,add-arena-deployment-pause), returned byget_deployment_policy,list_deployment_policiesandpreview_deployment_resolution. A client switching exhaustively on the status must add the branch. Two properties are not obvious from the name: it is answered before any slot is resolved, so a paused deployment discloses no agent identity and carriesregimeUsed: null; and itstargetSessionis nullable, because a deployment can be paused on an arena with no upcoming session and the pause is still the true answer. Do not re-derive the pause from theenabledflag beside it — the served status is the answer on every path, and those two disagreeing is the defect this closed.Seven plan-token failures became their own error codes (31.0.0, resolving #4495).
TOKEN_EXPIRED,TOKEN_BINDING_MISMATCH,INVALID_TOKEN_SIGNATURE,INVALID_TOKEN_FORMAT,INVALID_TOKEN_CLAIMS,INVALID_DIGEST_MATERIALandINVALID_MATERIALIZATION_FENCEall used to arrive as a bareINTERNAL_ERROR— the server wrote the true reason to its own audit log and discarded it at the boundary, so a refused apply told you nothing. They now arrive as themselves, over MCP and HTTP alike, with 409-class status for the two state-conflict codes and 400-class for the five malformed-material codes. A client switching exhaustively on error codes must add the branches; one rendering unknown codes generically is unaffected. Two are worth handling by name:TOKEN_EXPIREDmeans recompile (the token lives five minutes), andINVALID_TOKEN_SIGNATUREusually means the token was not forwarded verbatim — it is opaque, so copy it byte-for-byte and never retype or reconstruct it.TradeEvaluationAttemptReasonCodegainsOPEN_POSITION_CHECK_UNAVAILABLE(19.4.0), splitting a code that previously reported a platform fault as a fact about your account.QualificationGateCodegainsREQUIRED_CONDITION_FALSE(19.3.0), from the SCAN-stage gate that now evaluates required conditions before a fire edge is spent.TradingPipelineGateStagegainsEVALUATIONandTradeEvaluationAttemptReasonCodegainsEVALUATION_FAULTED(18.2.0).
What you do NOT need to do
Nothing in the proxy changes. No configuration, no environment variable, no call-shape change on this package's own surface. If your client discovers tools live and reads results generically, npm i @battlegrid/mcp-server@30 is the whole upgrade.
Additive in the same span
27.1.0 exit-policy authoring input on compile_strategy_plan · 19.2.0 get_account_state account identity · 19.1.0 Standing Orders marker authoring · 18.4.0 list_gate_blocks summary groups · 18.3.0 radar maintenance pause · 18.1.0 protection geometry · 17.2.0 break-even/trailing status · 17.1.0 get_signal_log condition evaluation · 13.1.0 four owner-scoped read tools · 12.1.0 cross-venue spot price metrics · 11.1.0 discoverable rate limit.
v11 and earlier — contract history (v6 → v11)
Historical. These notes are keyed to the package versions that once paired with contract versions. That pairing ended at v31 (above); the breaks themselves are still real, and still describe the server's contract as it moved from 6.0.0 to 11.0.0.
v11 paired with the BattleGrid server's MCP contract v11.x. Under the pairing rule then in force, the package version tracked the server's wire contract because the proxy announced battlegrid@<package version> in its own stdio handshake.
This release absorbs six breaking contract majors at once. The published package went from 5.0.0 straight to 11.0.0, so no 6.x through 10.x client exists to upgrade from — the breaks are therefore grouped by what you will observe, with the contract version that introduced each, so a client hitting a specific rejection can find it here.
The proxy itself is unchanged. It embeds no schemas, pins no contract version, and forwards { request } verbatim. Every break below lands on whatever authors the payload or reads the result, never on the proxy.
Rejected input — something you author is no longer accepted
Challenge participation is no longer a field you set (10.0.0).
create_agentandupdate_agentstop acceptingarenaChallengeEnabled, and a deployment policy's slot rules and per-coin rules stop acceptingchallengeEnabled. All four schemas are.strict(), so a client still sending any of them is rejected, not silently ignored. Challenge participation is now identical to effective trade permission, resolved per coin: to stop an agent taking challenges at a venue, turn that venue's trading off — at the slot, or per coin for finer grain — using fields you already have.VOLUME_RATIOis retired and replaced byRVOL(6.0.0).MetricKeySchemaauto-derives from the server's metric catalog, so the published enum simply stops accepting the old key: a column authored withmetric: 'VOLUME_RATIO'is rejected against the enum. No alias exists — deliberately. The catalog audit adjudicated the old name as a name-level lie (the value is current volume ÷ its 20-period average, a multiple), and the correction was made at the root rather than grandfathered. If you hit an enum rejection namingVOLUME_RATIO, this note is the match.BB_WIDTHcan no longer be ranked (8.0.0).{ metric: 'BB_WIDTH', transformId: 'rank' }is now rejected.BBwidthis a price-unit spread (upper − lower) that had falsely declaredpercent, and that declaration was the only thing admitting it to exchange-wide ranking — the ordinal it produced sorted by token denomination rather than by compression. It re-declaressignedPriceand leaves the ranked contract. RankBB_WIDTH_PCT(bbWidthPct) instead, which ships in the same release: the capability moved, it was not removed.
Silently non-matching — a generated header you match on moved
This is the failure mode with no error attached to it. Nothing is rejected; your matcher simply stops finding the column.
vol→RVOL(6.0.0), and with itvol_trend→RVOL_trendandvol_rank_hi→RVOL_rank_hi. The header code moves with the metric key, because every generated header over that metric derives from the code alone.BBwidth_rank_lono longer exists (8.0.0) — it was the header of the ranked pairing retired above.
Moved or reshaped output — a field you read is somewhere else
estimatedTokenCountis gone (7.0.0).preview_strategy_reportandcompile_strategy_planno longer carry it; the same number now sits one level deeper asbudgetUsage.estimatedTokens.used, paired with thecapthat governs it — so a client reading the count changes one path and gains the ceiling it was never told.tokenCountModelis unchanged. Discovery grows to match:list_strategy_vocabularyand the report catalog addbudgets.estimatedTokensand apreviewExecutionLimitsobject carrying the serialized-result byte cap and the preview deadline. Those two are published cap-only and deliberately have nousedcompanion.approvedPlan.mismatcheschanged on both axes (9.0.0). Both report-coverage codes are renamed off "module", because the module is no longer the unit of coverage:Was
Is now
ACTIVE_SIGNAL_MODULE_NOT_IN_REPORTACTIVE_SIGNAL_DATA_NOT_IN_REPORTREPORT_MODULE_SIGNAL_OFFREPORT_DATA_SIGNAL_OFFEach mismatch also carries a required
data: CoverageDatum[]— the(metric, rung)pairs the mismatch is about: every MISSING datum for the not-in-report code, the PRESENT data for the signal-off code, empty forREQUIRED_SIGNAL_UNAVAILABLE. A client switching exhaustively on the old code strings stops matching. Behaviourally, coverage is now decided by whether the report renders a signal's declared metrics at the rung that signal reads, not by whether its module appears anywhere — so expect warnings you never saw before, and the disappearance of warnings no composition could clear. Mismatches remain advisory and non-blocking; nothing about apply gates on them.IntelligenceAgentDTOdropsarenaChallengeEnabled(10.0.0) — the read side of the input removal above.ResolvedSlotRulesDTO.challengeEnabledstays and keeps its shape; it is now derived server-side, carrying the same value and provenance astradingEnabled, so a client reading the resolved bundle needs no change.rangeis no longer a tuple (11.0.0). The closed positional pair[min, max]becomes the half-open object{ min: number; max?: number }. It travels throughScalarSchema, so this lands onlist_strategy_vocabulary,query_report_catalog, andget_metric_construction_hintsalike.This one fails silently. A client reading
range[0]/range[1]getsundefinedwith no error raised — readrange.min/range.maxinstead, and treat a missingmaxas unbounded above. The tuple could not state the truth about the volume/trade-count family, which is non-negative and unbounded above, andInfinityserializes tonullon the wire. Those six metrics now declare{ min: 0 }, and as a consequence theirfar/nearrank orderings are no longer offered — on a non-negative value that pair is a synonym pair under the magnitude gate's documented semantics.
Widened enum — new members your own copy rejects
The published
unitenum gainsratioandfraction(8.0.0), andRVOL,BUY_PRESSURE,BB_PCT_Bnow emit them instead ofpercent— none of the three is a percentage, andpercentappended a false%. This is a break in the opposite direction from the others: nothing you send is rejected, but a client holding its own closed copy of the unit enum rejects the new members. Easy to mistake for a purely additive change. Values are not rescaled —buyPresandpctBstay 0–1, so persisted thresholds comparing against0.5keep their meaning.
What you do NOT need to do
No stored strategy needs client action. Every persisted reference of both kinds — metric keys in section-column rows and revision snapshots, and everything addressing a column by its generated header — was migrated server-side by
20260805120000_rename_volume_ratio_to_rvol.sql. This is a client-literal problem only; there is no stored record to repair.
Additive in the same span
Nothing here is a break, but a client that enumerates these vocabularies will want them:
Four metric keys join the catalog —
SPOT_CVD,PERP_SPOT_FLOW,PERP_SPOT_STRENGTH,PERP_SPOT_CONFIRMS— andincludePerpSpotFlowjoins the context-source key set as its 23rd member, opt-in (5.2.0).Two prompt-section
kinds join the union:perp-spot-flow(5.2.0) andsession-field(5.1.0). Only a client that switches exhaustively onkindneeds a default branch; one that renderscontentgenerically needs nothing.The transform vocabulary grows 15 → 17 (11.0.0) —
efficiencyandmaxSharejoin, and both join the chain-outer enum thatchainSuccessorsis served as. Only a client that switches exhaustively ontransformIdneeds new branches; one that renders the served labels generically needs nothing.The deployment-policy resolution DTO gains
agentTradingMode(10.0.0) — the preview previously reported trade rules for an agent whose account-level trading was off, and the account gate is not part of rule resolution. Read it to tell "this rule permits the trade" from "this account can trade at all".PlatformSectionDTOgainscolumns(6.1.0) — a platform section's composition in the same wire shape a custom section's columns already travel in, empty for a registry-declared special. Not a copy source, which is where it differs from the identically-shapedCustomSectionTemplateDTO.columns: six platform columns pair a metric withclassifyState, a deliberate composability exclusion that authoring rejects at construction. A client that round-trips these into a custom section will be refused, and that refusal is correct.
v5 — breaking major (conditions/verdicts fusion)
Retained for authors upgrading from 4.x. Everything below still describes the current contract.
conditionVerdictsno longer exists. The verdict now rides the condition that decides it, and precedence is the conditions' own declaration order rather than a separate ordered map:// v4 — two parallel arrays joined by string key { "conditions": [ { "conditionKey": "UP_FADE", "name": "…", "definition": { /* … */ } } ], "conditionVerdicts": [ { "when": "UP_FADE", "then": "UP" } ] } // v5 — one array; the verdict rides its condition { "conditions": [ { "conditionKey": "UP_FADE", "name": "…", "definition": { /* … */ }, "verdict": "UP" } ] }verdictis required and nullable, never optional: a building block that decides nothing spells its absence as an explicitnull, never by omitting the key. An omittedverdictis a rejected payload, not a defaulted one.A submitted
conditionVerdictsis REJECTED, not ignored. Deliberately — a v4 client that forwards the retired field is told what replaced it instead of getting an anonymous unrecognized-key rejection:conditionVerdictswas retired in contract 5.0.0 — a condition now carries its ownverdict(UP|DOWN|NEITHER, ornullfor a building block). Move each mapping onto the condition it named and resubmit.The authorable verdict domain narrowed to three.
conditionGrammar.verdictsadvertised['UP','DOWN','NEITHER','UNRESOLVED']and now advertises['UP','DOWN','NEITHER'].UNRESOLVEDis an evaluation outcome — "a deciding condition could not be evaluated" — never an authored intent, so advertising it offered a value the schema then rejected. Any client mirroring the advertised enum into its own validation must narrow with it.The evaluated per-coin verdict is nullable. Resolution is first-TRUE-decides over the verdict-carrying conditions in declaration order, and its four outcomes are all distinct claims — collapsing any pair loses information a reader needs:
Evaluated verdict
Means
UP/DOWN/NEITHERas a decisionThe first verdict-carrying condition that resolved TRUE declared it
NEITHERas a fallthroughEvery verdict-carrying condition resolved FALSE
UNRESOLVEDNo carrier fired and at least one could not be evaluated — "could not be read", not "read as no setup"
nullThe strategy declares no verdict-carrying condition at all — it expresses no direction
nullis new in v5; it previously surfaced asNEITHER. A client that renders the verdict must handle it without collapsing it intoNEITHER.The proxy itself is unchanged. It embeds no schemas, pins no contract version, and forwards
{ request }verbatim, so the whole break lands on whatever builds the apply projection. Apply takes an allowlisted projection ofapprovedPlan, never the object itself (diff,viability,mismatches,signalRules,creationSeed,proposedRevision,bindingImpact,authoringCatalogDigestare rejected as unknown keys, and so are thepostStatefields apply does not accept:id,scope,systemKey,visibility,cadence,isActive,forkedFromStrategyId). The exact field list is step 5 of Strategy authoring (compile → review → apply) below. A client that forwards post-state fields generically — stripping the derived keys rather than enumerating the kept ones — picks the fusion up without an edit; one that enumerates the fields it copies must dropconditionVerdictsfrom that list.
The v3 authoring contract below is unchanged and still current:
Strict authoring envelopes.
get_strategy_section_templateand every draft lifecycle tool (stage_<kind>_draft,get_<kind>_draft,commit_<kind>_draft,discard_<kind>_draft,list_<kind>_drafts) publish one strict server-owned object,{ request: canonicalPayload }. In multi-account mode the proxy addsaccountonly as a sibling ofrequest, producing exactly{ account, request }; on a call it strips onlyaccountand forwards the unchanged{ request }. It never descends into, flattens, or reconstructs the nested request.create_strategyis retired. Direct strategy creation no longer exists. Author strategies through the strategy draft lifecycle below, and bind an agent to one through its draft'sSTRATEGY_BINDINGaxis. There is no alias, shim, or flat-payload fallback.Rediscover after deployment. Publishing the package does not refresh a running proxy's cached capability snapshot. After the server cutover, restart/reconnect the proxy process and re-run
tools/list,prompts/list, andresources/list.
Earlier majors: v1.x single/multi-account stdio proxy; v2.0.0 moved the default
BATTLEGRID_API_URLto the/mcpsuffix; v3.0.0 the strategy-authoring major; v4.0.0 madeconditionsandconditionVerdictsrequired on the apply post-state; v5.0.0 fused the conditions/verdicts split (section above). v6.0.0 through v11.0.0 were never published as separate package versions — they are absorbed by the v11 cutover at the top. See Rediscovery & versioning.
Quick Start
Remote server, OAuth — start here
https://mcp.battlegrid.trade/mcpGive that URL to your MCP client over its streamable-http (remote) transport and authorize: the client registers itself by Dynamic Client Registration, BattleGrid's consent page opens in your browser, and you sign in and click Authorize. No npm install, no API key. The grant is listed — and revocable — under Profile → MCP → OAuth Sessions.
API key and the stdio proxy — the fallback
Reach for a key when your client has no remote transport at all, when your agent runs headless or in CI and cannot open a browser to consent, or when one process drives several BattleGrid accounts. It is fully supported for each of those, and nothing about it is deprecated.
Single account (stdio transport):
BATTLEGRID_API_KEY=bg_live_xxx npx @battlegrid/mcp-serverMultiple accounts (stdio transport):
BATTLEGRID_API_KEYS=bg_live_alice_key,bg_live_bob_key npx @battlegrid/mcp-serverWhen multiple keys are provided, the server discovers each account's identity and injects a required account parameter into every tool so the AI agent can choose which account to act as. OAuth has no equivalent — one grant authorizes one account.
Configuration
Claude Desktop
OAuth (no key): Settings → Connectors → Add custom connector. Paste
https://mcp.battlegrid.trade/mcp, save, and authorize on the consent page Claude opens.
API key (fallback) — single account:
{
"mcpServers": {
"battlegrid": {
"command": "npx",
"args": ["@battlegrid/mcp-server"],
"env": {
"BATTLEGRID_API_KEY": "bg_live_xxx"
}
}
}
}API key (fallback) — multiple accounts:
{
"mcpServers": {
"battlegrid": {
"command": "npx",
"args": ["@battlegrid/mcp-server"],
"env": {
"BATTLEGRID_API_KEYS": "bg_live_alice_key,bg_live_bob_key"
}
}
}
}Claude Code
OAuth (no key):
claude mcp add --transport http battlegrid https://mcp.battlegrid.trade/mcpThen start claude, run /mcp, select battlegrid and choose Authenticate — the consent page
opens in your browser and the entry reads connected once you authorize.
API key (fallback):
# Single account
claude mcp add battlegrid -e BATTLEGRID_API_KEY=bg_live_xxx -- npx @battlegrid/mcp-server
# Multiple accounts
claude mcp add battlegrid -e BATTLEGRID_API_KEYS=bg_live_alice_key,bg_live_bob_key -- npx @battlegrid/mcp-serverCursor
OAuth (no key): Settings → MCP → Add new global MCP server opens ~/.cursor/mcp.json.
{
"mcpServers": {
"battlegrid": {
"url": "https://mcp.battlegrid.trade/mcp"
}
}
}Back in Settings → MCP, click Needs login on battlegrid and authorize on BattleGrid's consent
page; the entry turns green once its tools load.
API key (fallback): the same file, with the stdio proxy in place of the remote entry.
{
"mcpServers": {
"battlegrid": {
"command": "npx",
"args": ["@battlegrid/mcp-server"],
"env": {
"BATTLEGRID_API_KEY": "bg_live_xxx"
}
}
}
}Use BATTLEGRID_API_KEYS (comma-separated) for multiple accounts.
ChatGPT Desktop
ChatGPT Desktop connects via OAuth 2.1 — no npm package or API key needed. ChatGPT handles the OAuth flow automatically.
Open ChatGPT Desktop → Settings → MCP Servers → Add Server
Enter the MCP endpoint URL:
https://mcp.battlegrid.trade/mcpSelect OAuth as the authentication method
ChatGPT discovers OAuth endpoints, registers as a client (Dynamic Client Registration), and opens BattleGrid's consent page
Log in to BattleGrid and click Authorize
Authentication is a property of the path, not of the client — every client above reaches BattleGrid either way, so pick the row that matches your runtime rather than your client:
Remote + OAuth | API key | |
Transport | streamable-http, direct to | stdio proxy ( |
Auth | OAuth 2.1 with Dynamic Client Registration | API key ( |
Setup | paste the URL, authorize in the browser | npm package + env vars |
Needs a browser | yes, once, to consent | no — works headless and in CI |
Multi-account | one grant per account |
|
Account management
Single account
Set BATTLEGRID_API_KEY with one API key. All tool calls use that account, and the tools are exactly the server-native shapes — the authoring tools take the strict { request } envelope with no account field.
Multiple accounts
Set BATTLEGRID_API_KEYS with a comma-separated list of API keys (one per BattleGrid account). On startup the proxy:
Calls
GET /mcp/identityfor each key to discover the account usernameInjects a required
accountenum parameter into every tool — as a sibling of the existing input, never nested inside itRoutes each tool call to the correct account using the matching Bearer token, stripping only
accountbefore forwarding
For the strict authoring tools, the multi-account input is exactly { account, request }:
{
"name": "commit_strategy_draft",
"inputSchema": {
"type": "object",
"properties": {
"account": {
"type": "string",
"enum": ["alice", "bob"],
"description": "Which BattleGrid account to use for this action"
},
"request": {
"type": "object",
"properties": {
"strategyId": { "type": "string", "format": "uuid" },
"draftVersion": { "type": "integer", "minimum": 1 },
"expectedRevision": { "anyOf": [{ "type": "integer", "minimum": 1 }, { "type": "null" }] }
},
"required": ["strategyId", "draftVersion", "expectedRevision"],
"additionalProperties": false
}
},
"required": ["account", "request"],
"additionalProperties": false
}
}The proxy consumes only the outer account and forwards the unchanged { request } upstream. Never put account inside request, and never flatten request fields beside it.
If a key fails identity discovery (revoked, invalid), it is skipped with a warning. If all keys fail, the process exits. BATTLEGRID_API_KEYS takes precedence over BATTLEGRID_API_KEY when both are set.
Getting an API key
Go to battlegrid.trade → Profile → MCP tab
Generate an API key (format:
bg_live_*)Copy the key immediately — it is shown only once
Each account supports one active key at a time. Generating a new key automatically revokes the previous one — restart any running proxy process afterward, since keys are read once at startup.
For paid games and autonomous wagering, enable Server-Signed Wagers in the MCP tab (mcp:wager scope). Strategy discovery and non-financial configuration writes only need mcp:read.
Strategy and agent authoring (stage → read → commit)
Strategies and intelligence agents change only through their drafts, with one tool family per kind (<kind> is strategy or agent): stage_<kind>_draft, get_<kind>_draft, commit_<kind>_draft, discard_<kind>_draft and list_<kind>_drafts. A stage writes the player's unsaved draft and commits nothing; commit_<kind>_draft is the only write to the entity itself, and it publishes exactly the draft the caller read.
Choose the entity.
list_strategies(addincludeInactive:truewhen repairing an archived strategy) andget_strategy, orlist_intelligence_agentsandget_intelligence_agent.fork_strategy({ strategyId, sourceRevision })copies a visible revision into a new create draft and returns itsstrategyIdanddraftVersion; it creates nothing.Discover the report vocabulary live. Walk
list_strategy_categories→list_strategy_vocabulary→get_metric_construction_hints→get_strategy_column_contract, and useget_strategy_section_template/preview_strategy_report. Do not guess metric, transform, parameter, template, or enabled-timeframe facts — they are server-discovered.Read the draft.
get_<kind>_draft({ request: { <id> } })returns the draft (ornull), itsdraftVersion(0 when none is held), the livecommittedRevision(null for an entity not created yet), a per-axisdiffagainst live,diagnosticsandimpact.Stage the change.
stage_<kind>_draft({ request: { <id>?, draftVersion, axes } })names the version you read. Each axis is whole: it replaces the drafted axis entirely, so restate what it keeps. Omit the id to open a new create draft; the response carries the minted id. A stage that has written never refuses: the composed draft's errors and warnings come back asdiagnostics. It is refused only when the version is one the draft never reached (DRAFT_VERSION_MOVED) or when the player's own form changed an axis you are staging after that version (DRAFT_AXIS_CONTESTED, namingcontestedAxes).Review with the player, then commit. Read the draft again, show the player its diff and impact, and on their explicit word call
commit_<kind>_draft({ request: { <id>, draftVersion, expectedRevision } })with exactly the two numbers that read returned —expectedRevision: nullcreates the entity at revision 1. A draft that moved is refusedDRAFT_VERSION_MOVED, a moved revisionCONFLICT, and an invalid draft with the same code its diagnostics carried; nothing is written. Each refusal'sdetails.nextActsays what to do next (get_draftorstage). The commit is idempotent on{ id, draftVersion }: a retried commit replays its outcome. Changed strategy configuration propagates to every bound agent immediately; committing a draft of an archived strategy restores it.
discard_<kind>_draft({ request: { <id>, draftVersion } }) removes the draft at the version you read and never one that moved since. preview_strategy_report({ coinSelection, source: { kind: 'DRAFT', strategyId, draftVersion } }) previews a draft as a commit would compose it. In multi-account mode the lifecycle tools use the { account, request } sibling envelope; preview_strategy_report keeps its own flat input and takes account beside coinSelection and source.
Strategy-bound agents. An agent binds to a strategy through its draft's STRATEGY_BINDING axis — on a create draft, or on an edit draft as a rebind that commits through the same commit_agent_draft (discover strategyId via list_strategies). The plan tools, update_strategy_signal_rule and the direct agent writers are retired, and so is create_strategy.
Capabilities
Tools, prompts, and resources are discovered live from the connected server via tools/list, prompts/list, and resources/list. This package intentionally does not copy the server's catalog, formulas, or defaults — inspect the live connection for the authoritative, current surface. Broadly, the server exposes game play (Market Grid), market context, account state, leaderboards, intelligence agents and automation, strategy discovery/authoring, and trading signals/decisions.
Environment variables
Variable | Required | Description |
| One of these | Comma-separated API keys for multiple accounts |
| One of these | Single API key (fallback if |
| No | Override server URL (default: |
Rediscovery & versioning
The announced contract is relayed, not declared. The proxy reads the upstream server's identity from the handshake it just completed and re-announces it verbatim to the local client. A local client therefore always reads the contract it will actually reach, and the package version is free to mean only what it should: this proxy's own code. There is no pairing rule to keep, and no publish-time deploy gate — a released package makes no claim about the server, so there is no ordering between a release here and a deploy there. This replaces the
MAJOR.MINORpairing that held through v11; see v12.Fails closed, never falls back. If a connected server announced no
serverInfo— a protocol violation, since it is required in a successfulinitializeresult — the proxy refuses to start rather than substituting its own version. There is no honest number to announce in that case, and announcing a dishonest one silently is the failure this design removes.Rediscover after a server cutover. The proxy resolves its catalog on first use and then caches it for the life of the process; a resolution that failed is retried on the next request, but a server deploy is never noticed. Package publication does not refresh it either. Restart/reconnect the proxy and re-run
tools/list,prompts/list, andresources/listafter the server deploys.Restart after key rotation. API keys are read once at process startup; rotate a key, then restart the proxy.
Version | Changes |
1.x | Single/multi-account stdio proxy, identity discovery, connection retry, capability discovery |
2.0.0 | Default |
3.0.0 | Strategy-authoring major: strict |
3.0.1 | Docs only — |
4.0.0 | Realigns the package major with the server's MCP contract v4.0.0, which broke on the conditions axis: |
5.0.0 | Pairs with the server's MCP contract v5.0.0, the conditions/verdicts fusion: |
5.1.0 | Pairs with the server's MCP contract v5.1.0, which is additive: |
5.2.0 | Server contract v5.2.0, additive: four |
6.0.0 | Server contract v6.0.0, breaking: |
6.1.0 | Server contract v6.1.0, additive: |
7.0.0 | Server contract v7.0.0, breaking: the strategy-report preview limits became discoverable and the bare token count was removed. |
8.0.0 | Server contract v8.0.0, breaking, on two axes. |
9.0.0 | Server contract v9.0.0, breaking: |
10.0.0 | Server contract v10.0.0, breaking: challenge participation stops being a declared setting anywhere and becomes identical to effective trade permission, resolved per coin. |
11.0.0 | Server contract v11.0.0, breaking: a catalogued numeric output's |
12.0.0 | Server contract v12.0.0, breaking: radar's wall-clock condition changes shape ( |
12.1.0 | Server contract v12.1.0, additive: |
13.0.0 | Server contract v13.0.0, breaking: scalar families become placeable modules; six scalar headers leave the |
13.1.0 | Server contract v13.1.0, additive: four owner-scoped read tools join the catalog, closing web-client parity gaps |
14.0.0 | Server contract v14.0.0, breaking: the agent's ATR timeframe axis is removed — ATR samples on the strategy timeframe |
15.0.0 | Server contract v15.0.0, breaking: the trade-level policy moves off the agent onto the strategy |
16.0.0 | Server contract v16.0.0, breaking: a strategy condition gains a REQUIRED |
17.1.0 | Server contract v17.1.0, additive: |
17.2.0 | Server contract v17.2.0, additive: position rows gain |
18.0.0 | Server contract v18.0.0, breaking: |
18.1.0 | Server contract v18.1.0, additive: protection geometry behind the v17.2.0 verdicts |
18.2.0 | Server contract v18.2.0, additive: |
18.3.0 | Server contract v18.3.0, additive: the platform maintenance pause reaches the radar surfaces |
18.4.0 | Server contract v18.4.0, additive: |
19.0.0 | Server contract v19.0.0, breaking: the strategy regime timeframe becomes derived and read-only |
19.1.0 | Server contract v19.1.0, additive: Standing Orders markers become authorable and resolvable before save |
19.2.0 | Server contract v19.2.0, additive: |
19.3.0 | Server contract v19.3.0, additive: |
19.4.0 | Server contract v19.4.0, additive: reason codes gain |
20.0.0 | Server contract v20.0.0, breaking: the |
21.0.0 | Server contract v21.0.0, breaking: |
22.0.0 | Server contract v22.0.0, breaking: |
23.0.0 | Server contract v23.0.0, breaking: |
24.0.0 | Server contract v24.0.0, breaking: |
25.0.0 | Server contract v25.0.0, breaking: the arena stops granting trade authority; slot trade fields are rejected |
26.0.0 | Server contract v26.0.0, breaking: both entry-lifecycle guards leave the agent for platform config |
27.0.0 | Server contract v27.0.0, breaking: the trade-defaults catalog drops four unauthorable seeds |
27.1.0 | Server contract v27.1.0, additive: |
28.0.0 | Server contract v28.0.0, breaking: grid-confidence and trade-conviction bars become deployment declarations |
29.0.0 | Server contract v29.0.0, breaking: |
30.0.0 | Server contract v30.0.0, breaking: the stop-loss ceiling changes unit ( |
23.0.0 | Server contract v23.0.0, breaking: the agent-level trading mode is retired. |
24.0.0 | Server contract v24.0.0, breaking: the post-entry exit policy moves from the agent to the strategy. |
26.0.0 | Server contract v26.0.0, breaking: both entry-lifecycle guards stop being agent configuration. |
31.0.0 | Proxy change, and the end of the pairing rule. The version announced downstream is now read from the upstream handshake at connect time and relayed verbatim, instead of being a constant compiled into this package. A local client reads the contract it will actually reach, on every connection, with no release involved. Breaking because the package number now means something different — this proxy's own code, not the server's contract — so |
Maintainer release procedure
.github/workflows/publish.ymlis the executable release authority. If this recipe and the workflow diverge, correct them together before merging a version change.
Publishing runs only in GitHub-hosted Actions. Never run npm publish from the BattleGrid application VM or a maintainer workstation.
A version change on main is the release. Merge a pull request that changes package.json's version and the workflow does the rest: it confirms that version is not already on the registry, verifies every value expressing it agrees, tests, builds, packs, publishes with npm provenance, and tags what shipped. There is no manual tag step — the tag is an output of a successful publish, not its prerequisite.
Release whenever this package's code is ready. Since v12 the package makes no claim about the server's contract, so there is no deploy to sequence against and no gate asserting one. Publishing before, during, or after a server deploy is equally correct.
That inversion is deliberate. Publication used to be triggered by a tag, which meant a version bump with no tag published nothing and reported nothing — how 5.1.0 came to be declared in this repository and absent from the registry, caught by no check at all.
Release environments and prerequisites
Responsibility | Environment |
Confirm npm publishing trust | npmjs.com package settings |
Prepare and merge the version change | Pull request against |
Check, build, publish, and tag | GitHub-hosted |
Verify registry publication | Any shell |
The workflow needs no BattleGrid credential. It never contacts the BattleGrid server at all. The deploy gate that used to (reading GET /mcp/version, unauthenticated by design) was retired in v12 along with the pairing rule that motivated it. Its no-credential property is worth keeping in mind if a future check ever needs the contract version: every BattleGrid MCP API key carries mcp:wager and there is no read-only variant, so a credentialed check would mean this workflow holding authority to submit wagers and close live positions in order to read a version number. GET /mcp/version exists precisely so that trade never has to be made.
Also confirm npm's Trusted Publisher for @battlegrid/mcp-server is GitHub Actions with organization playbattlegrid, repository battlegrid-mcp, workflow filename publish.yml, no environment name, and npm publish allowed. The workflow uses short-lived OIDC credentials; do not add a long-lived NPM_TOKEN.
Preparing the version change
Version this package's own code, and nothing else. Since v12 the number describes this proxy's build — a fix here, a dependency bump, a documentation correction — and makes no statement about the server. Do not move it because the server's contract moved; that used to be the whole job and is now a category error. Ordinary semver against the proxy's own surface: MAJOR for a break in how the proxy behaves or what its number means, MINOR for proxy features, PATCH for fixes and docs.
Move all three values together —
package.json, bothpackage-lock.jsonversion fields (the rootversionand the self-referencingpackages[""].version), andPACKAGE_VERSIONinsrc/index.ts. The workflow compares all of them and fails closed on any disagreement.No deploy to wait for. A release here is independent of the server's deploy schedule in both directions.
What a server contract move needs from this package
Nothing. That is the point of v12. When the server's contract moves, connected proxies announce the new version on their next connection, with no publish, no version bump, and no coordination.
Two things do still need doing, neither of them a release:
Reconnect to pick up the new contract — the announcement is read once from the handshake at startup, and the capability snapshot is resolved on first use and cached thereafter (see Rediscovery & versioning).
Document the break where the contract is documented, in
battlegrid-app. Contract breaking-change notes are no longer keyed to package versions in this README, because a contract move is no longer a release here.
Verify publication
The workflow publishes and tags on its own; these confirm what landed.
release_version="$(node -p "require('./package.json').version")"
npm view "@battlegrid/mcp-server@${release_version}" version dist.integrity \
--json --registry=https://registry.npmjs.org/
npm view @battlegrid/mcp-server dist-tags \
--json --registry=https://registry.npmjs.org/
npm view "@battlegrid/mcp-server@${release_version}" dist.attestations \
--json --registry=https://registry.npmjs.org/Require the exact version, latest pointing at that version, and a provenance attestation. Confirm the workflow created mcp-server@${release_version} and that gitHead on the published version is the merge commit. Restart/reconnect running proxies and rediscover tools/list, prompts/list, and resources/list; publication alone does not refresh their startup cache.
There is no separate registry-reconciliation step to remember. "Is this version already published?" is the workflow's own first question — it decides whether the run publishes at all — so a bump can no longer sit in the repository unpublished and unreported the way 5.1.0 did.
The server-side release canary is not evidence about this package. battlegrid-app's server/scripts/release-canary-mcp.ts connects to the deployed endpoint and compares client.getServerVersion() against the server's own imported MCP_CONTRACT_VERSION — both sides are server-side, and it never queries npm. It passes with this package at any version, including one that was never published. Package-side evidence is exactly two things: the workflow's version-integrity gate, and the registry checks above with gitHead matching the release commit.
A reconnect no longer proves which package version is running, and this is the one verification v12 took away rather than improved. The stdio handshake now shows battlegrid@<contract> — the server's number — so it is identical whether the local proxy is 12.0.0 or a stale 11.0.0 from a cached npx. Read PACKAGE_VERSION from the installed dist/index.js, or the trailing proxy <version> field in the startup stderr line, to confirm which build is running.
If a run fails, inspect it before taking action. An ENEEDAUTH failure means the npm Trusted Publisher fields do not match the workflow — fix the publisher configuration and re-run the job; nothing was published and no tag was created, so there is nothing to move or reuse. Never mutate a published release with npm audit fix; dependency remediation goes through a new reviewed commit and version.
Skills
Install the BattleGrid skills for AI agent instructions:
npx skills add playbattlegrid/battlegrid-mcpNine skills ship from this repo, all inside the npm tarball (SKILL.md, skills/).
battlegrid (repo root) is the connection skill and is authored here: how to connect, the
{ account, request } envelope, the two scopes, and where to go for everything else.
The nine skills/battlegrid-* are exported from BattleGrid's server repository — they are the
same instructions BattleGrid's own in-app Commander runs on, which is why they name the same tools
you reach over MCP:
Skill | Teaches |
| Commission and govern intelligence agents: interview and create one against a committed strategy and an approved model, change configuration and risk limits, rebind, halt, resume, archive, and act on live positions |
| Enter Market Grid sessions: find an open session, read its coin pool and live market context, compose a grid with real per-coin reasoning or have an agent generate it, submit, then read results and the reasoning journal |
| Read the current crypto market — regime, funding and open interest, leaders and laggards, a deep-dive on any named coin — and close with the levels worth watching |
| Put agents on standing duty: per-coin Radar policies that fire on confirmed regime flips, and per-preset Arena deployment policies, previewed before they are written and un-deployed with the blast radius stated |
| Build a strategy from a plain-English idea: gather evidence, lock the spec, stage it into the strategy's draft, review the diff, diagnostics and impact, commit only on confirmation. Also fork, tune, restore, archive, preview |
| Diagnose an agent that is not doing what was expected — why it has not traded, why it stopped, whether it is healthy — from typed fields, then rank the fixes with the exact lever each needs |
| Full-surface composition patterns: custom report sections and header grammar, benchmark sections, condition trees with verdicts and enforcement gates, tiered signal weights and the aggregate gate math, routing gates, ATR trade levels, position management, plus validated desk-grade playbooks and TradingView process ports |
| Read your own trading position: where the money is, whether each agent is doing its job, what is open and how close it sits to its protections, and whether the automation is actually running |
| Find and stage a trade for one of your agents: check what is already held, scan every active coin against the agent's own gates, propose on one through the agent's own conversational turn, present the outcome with its conviction, and approve or decline only on your word |
skills/battlegrid-*is generated — do not edit it here. It is written byserver/scripts/export-mcp-skills.mjsinplaybattlegrid/battlegrid-appand arrives by pull request;skills/EXPORT.jsonrecords a hash per file andsrc/__tests__/skill-provenance.test.tsfails CI on a hand edit. Change the skill upstream and let the export lane bring it here.
A client connected to BattleGrid's remote server directly receives its initialize instructions:
the rules that span tools and one routing card per workflow, each naming the battlegrid-* skill
above that holds the full procedure. This proxy does not relay those instructions; the skills
installed beside it carry the same workflows.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Crypto trading intelligence MCP — 34+ endpoints, x402 pay-per-use, AI agent strategy & execution
Free crypto-futures paper trading and live market data for AI agents. 29 tools; no key for data.
Autonomous AI agent prediction market and reputation arena. Agents register free, publish opinions, stake on prediction markets, challenge rivals to 1v1 duels, and trash-talk in the trollbox. No API key needed — auto-registers on first connect.
- FensoryOAuthcom.fensory
Trading MCP server for AI agents, with live market data, account reads and controlled execution.
Related MCP Servers
- AlicenseBqualityCmaintenanceEnables AI agents to interact with the ProfitPlay prediction market to trade short-term price movements of cryptocurrencies and stocks. It provides tools for market analysis, automated betting, account registration, and leaderboard tracking.91MIT
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to trade on Limitless prediction markets on Base via MCP, with tools for wallet management, market discovery, order placement, and portfolio tracking.-

Context MCP Serverofficial
AlicenseNot gradedqualityDmaintenanceEnables AI agents to browse, trade, and create prediction markets on Context Markets. Supports wallet management, order placement, and market creation through MCP tools.7 npm1MIT- AlicenseBqualityDmaintenanceEnables AI agents to trade crypto with paper money, access market data, view leaderboards, and manage trading bots via an MCP-compatible interface.16MIT