@battlegrid/mcp-server 31.2.24 → 31.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,104 @@ Seeing package `31.x` alongside handshake `battlegrid@33.x` — the package **be
24
24
 
25
25
  **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.
26
26
 
27
+ ## Contract history — v60
28
+
29
+ One release train, four numbers, and two parts to read first: **`propose_entry_decision` no longer
30
+ 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
31
+ every contract move is `docs/architecture/MCP_CONTRACT_HISTORY.md` in `battlegrid-app`, and the
32
+ served version is what the handshake announces.
33
+
34
+ ### Rejected input — something you author is no longer accepted
35
+
36
+ - **The entry axis names no bar but the strategy's own, and three of its keys are gone** (60.0.0,
37
+ `decide-entry-on-strategy-close`). The authoring schemas are `.strict()`, so `compile_strategy_plan`,
38
+ `apply_strategy_plan`, `fork_strategy` and `restore_strategy` REFUSE a body carrying
39
+ `entry.confirmTf` (the deciding bar is the strategy's own timeframe, so a required input whose only
40
+ legal value was another field of the same strategy is absent rather than mirrored), `entry.closes`
41
+ and `entry.bandAtrMultiple` (a multi-bar hold is declared on the condition that needs it; the
42
+ displacement band is replaced by the platform's own entry-deviation gate, measured against the
43
+ decided close), and the whole `exit` object (the open-position exit lane judges one closed candle
44
+ of the position's strategy timeframe). Exactly one input hash moves, `compile_strategy_plan`'s.
45
+
46
+ - **`entry.trigger: AT_SIGNAL` is retired for authoring** (60.0.0, `retire-at-signal-trigger`, riding
47
+ the same number). Refused on every authoring surface and on a RESTORE, which rebuilds a stored
48
+ revision through the same value object. The enum member stays READABLE on every strategy read and
49
+ on a fired decision's provenance, so a pre-retirement revision is still legible — it just cannot be
50
+ re-authored. The three that remain are `ON_CANDLE_CLOSE`, `STOP_THROUGH_LEVEL` and `ON_RETEST`.
51
+
52
+ ### Changed meaning, unchanged shape
53
+
54
+ - **`tradeStatus` follows NET P&L on every closed trade** (60.3.0, `label-trade-outcome-by-pnl`).
55
+ No schema hash moves for this and the values you receive change anyway. It used to map the close
56
+ REASON onto a verdict — every `TAKE_PROFIT` was `WON`, every `STOP_LOSS` was `LOST`, and a
57
+ `MARKET_CLOSE` of either sign was the neutral `CLOSED` — so a stop that filled after a break-even
58
+ reprice was reported as a loss and a take-profit eaten by fees as a win. `LIQUIDATED` still
59
+ outranks the number, because a force-close is not a verdict about the trade; `CLOSED` now means
60
+ only that there is no outcome row to judge. A client that counted `WON` rows was counting
61
+ take-profits.
62
+
63
+ - **A proposal is QUEUED, not decided** (60.2.0, `queue-manual-entry-for-close`). This is the entry in
64
+ this section to act on. `propose_entry_decision` registers a request against the agent's next
65
+ strategy-bar close and returns immediately: no model runs, nothing is spent, and the call carries
66
+ `type: "queued"` with `request` — `requestId`, the bar (`barStart`), when the answer is due
67
+ (`decidesBy`) and the last instant that bar may still be decided (`windowEndsAt`). The answer
68
+ arrives later, in the agent's conversation and, when it proposes a trade, in
69
+ `list_pending_approvals`. A close that does not qualify, a window that passes with no sweep, and a
70
+ bar the agent's own radar deployment decided first are each recorded in the conversation instead.
71
+
72
+ `recommendation` and `no_trade` REMAIN in the union: the idempotency registrar replays results
73
+ recorded before this release for their TTL, so a client that dropped those members would fail on
74
+ its own retry. **A client written against 60.2 handles `queued` and `error`**; one that must also
75
+ replay handles all four.
76
+
77
+ ### Reshaped output — the same call returns a different shape
78
+
79
+ - **The four retired entry keys leave every strategy read** (60.0.0) — `get_strategy`,
80
+ `list_strategies`, `fork_strategy` and both plan envelopes — and `confirmTimeframesByMainCandle`
81
+ leaves `list_strategy_vocabulary`, because there is no confirm set left to publish.
82
+
83
+ - **`entryDiscipline.closes` and `.bandAtrMultiple` go `number` → `number | null`** (60.0.0) on
84
+ `get_trade_outcome_by_decision` and `list_trade_outcomes`. Null on every decision fired after this
85
+ release, which authors neither; a non-null pair dates the row to the arming era.
86
+
87
+ ### Widened enum — new members your own copy rejects
88
+
89
+ - **`TradeConvErrorCode` gains `REQUEST_PENDING`** (60.2.0), on the SURFACE arm of
90
+ `propose_entry_decision`'s error: one pending request per user and coin, so a second is refused. A
91
+ coin already carrying a pending or live position is refused before anything is queued, as an
92
+ ENGINE-origin `OPEN_POSITION_CONFLICT` — a member that was already published, reaching this surface
93
+ for the first time.
94
+
95
+ ### Additive in the same span
96
+
97
+ - **The exit names the leg that filled** (60.3.0, `label-trade-outcome-by-pnl`). `TradeOutcomeDTO`
98
+ and the pipeline outcome summary gain `exitRepriceSource`: the reprice that placed the protection
99
+ leg which actually closed the position — `BREAK_EVEN`, `TRAILING`, `TIME_DECAY`,
100
+ `MANUAL_OVERRIDE`, `UPDATED` — or null on every close no protection leg filled and on a leg that
101
+ was never repriced. It is the mechanism behind the verdict above, so read it beside `closeReason`
102
+ before calling a stopped-out trade a failure. The enum is the one `get_position_audit_history`
103
+ already publishes. Four output hashes move; no input schema does.
104
+
105
+ - **Two tools join the catalog for the request lifecycle** (60.2.0, `queue-manual-entry-for-close`).
106
+ `get_entry_request` (read scope) reads a request that is still pending and is `NOT_FOUND` once it
107
+ has been answered, cancelled or expired — the answer is in the conversation, not there.
108
+ `cancel_entry_request` (`mcp:wager`) withdraws one before its bar is decided. `toolCount` 115 → 117.
109
+
110
+ - **Three radar reads publish the close decision** (60.1.0, `add-radar-close-decision-state`).
111
+ `get_radar_deployment`, `list_radar_deployments` and `preview_radar_resolution` carry one further
112
+ key on `resolvesNow`: `closeDecision`, non-null whenever an agent is on duty for the pair. It
113
+ carries the deciding `timeframe`, the `nextCloseAt` instant, a `state` of `WAITING` or `DEFERRED`
114
+ (a bar has closed and no sweep has decided it yet), and `last` — the bar, the instant, the outcome
115
+ (`FIRED` / `NOT_QUALIFIED` / `MISSED`), the gate and the closed reading's score against its
116
+ minimum — or null before the pair's first close decision.
117
+
118
+ **Read it first on a close-deciding pair.** The sibling `qualified` and `qualificationBlock` fields
119
+ are the per-minute DISPLAY reading there and decide nothing, so an agent ranking them reports
120
+ "qualified — watching for a setup" about a pair whose last three closes were each refused. Neither
121
+ field is removed or reshaped. A `NOT_QUALIFIED` outcome with a NULL gate beside a score at or above
122
+ the minimum is the consumed edge — the close qualified and the baseline was already spent — stated
123
+ by the server so no client compares the two numbers.
124
+
27
125
  ## Contract history — v37 → v54
28
126
 
29
127
  Eleven majors reached authors while this section stopped at v36. That gap is the mechanism, not an
package/dist/index.d.ts CHANGED
@@ -49,7 +49,7 @@ import { type Implementation, type Prompt, type Resource } from '@modelcontextpr
49
49
  * being asked. Move it for a change to THIS package — a proxy fix, a dependency bump, a docs
50
50
  * correction. Never move it to track the server.
51
51
  */
52
- export declare const PACKAGE_VERSION = "31.2.24";
52
+ export declare const PACKAGE_VERSION = "31.2.26";
53
53
  export declare const DEFAULT_URL = "https://mcp.battlegrid.trade/mcp";
54
54
  export interface EnvConfig {
55
55
  apiKeys: string[];
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema
52
52
  * being asked. Move it for a change to THIS package — a proxy fix, a dependency bump, a docs
53
53
  * correction. Never move it to track the server.
54
54
  */
55
- export const PACKAGE_VERSION = '31.2.24';
55
+ export const PACKAGE_VERSION = '31.2.26';
56
56
  export const DEFAULT_URL = 'https://mcp.battlegrid.trade/mcp';
57
57
  const MAX_RETRIES = 3;
58
58
  const RETRY_DELAYS_MS = [2000, 4000, 8000];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@battlegrid/mcp-server",
3
- "version": "31.2.24",
3
+ "version": "31.2.26",
4
4
  "description": "BattleGrid MCP server — play crypto prediction games from AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "generator": "battlegrid-app/server/scripts/export-mcp-skills.mjs",
3
- "contractVersion": "59.0.0",
3
+ "contractVersion": "60.3.0",
4
4
  "files": {
5
5
  "battlegrid-agent-management/SKILL.md": "b6ea75b3d838c1dbaaf3984d7e99a11c81686de2c1a1910c35699ee7e70dafa5",
6
6
  "battlegrid-arena-play/SKILL.md": "03fa153bf82be18bf5ed01e3ba12e2cd9f99a81b08d903a77b52169e7519a182",
7
7
  "battlegrid-market-analysis/SKILL.md": "22fde9c4eac0261c89a8056ab8b20b66fcbe1fd58447fa0ac84fd410b4d134d7",
8
8
  "battlegrid-radar-deployment/SKILL.md": "d2a012b154e1e529db19529bd7c3d75d069f4e410d5804add51165f0f0902b2e",
9
- "battlegrid-strategy-authoring/SKILL.md": "1f1bf1ecfbf1a20d585a53e8f1ba8e9fbdeaa329cad9ebdc16ce6cac2b213358",
9
+ "battlegrid-strategy-authoring/SKILL.md": "ea2593ea9d6d2feb9910e285e4b132eb36001164b9d6551cc206531afad3f552",
10
10
  "battlegrid-strategy-doctor/SKILL.md": "9ab3f79128ec81d3e90167c3f1ea4bd6f6b570290e5ebe2bf855195263c58239",
11
- "battlegrid-strategy-examples/SKILL.md": "6f15da8b29a8aff15f6829d7ae87cbe74904bfe5372e37fe17d578cc0c289577",
11
+ "battlegrid-strategy-examples/SKILL.md": "263c3b865ea85e56ac420611a7070360d65be6f2faee071329880e49a6101eda",
12
12
  "battlegrid-strategy-examples/references/playbooks.md": "35c543c090d00ba19e4bcf0a80502afac7a4954643fc535de0b24381c91ecbf4",
13
13
  "battlegrid-strategy-examples/references/recipes.md": "53887daece17995543e7d3fe1bf2577fc41a6254c291acacde48d7de63853641",
14
14
  "battlegrid-strategy-examples/references/tradingview-ports.md": "2b13adbc137178955edcca31b6ab40c01c745a932a0750b91db8820c736700bb",
15
15
  "battlegrid-trade-analysis/SKILL.md": "32573a67e55d735e6ee4e36b3b48c815694102704bc453c4d1c13db39551b3da",
16
- "battlegrid-trade-proposal/SKILL.md": "311af5a91576b095dcfcb3e3fc8b41e8a75d7af505222f975fde9733c633cb7e"
16
+ "battlegrid-trade-proposal/SKILL.md": "503ed705d4f9823e21846afe5a10f35d7d6e35933c4bdd1d5b4fd923eb9d674d"
17
17
  }
18
18
  }
@@ -100,11 +100,13 @@ question you would otherwise guess — and a refusal you would otherwise earn:
100
100
  yourself on a CREATE is refused with a hint telling you to omit `sectionKey` on CREATE; on an
101
101
  UPDATE, send back the key the compile returned. This is the one composition field whose right
102
102
  answer is "leave it out".
103
- - **The `entry` axis is required on every CREATE**, all six keys, no defaults — `trigger`,
104
- `confirmTf`, `closes`, `bandAtrMultiple`, `levelOffsetAtrMultiple`, `validForBars`. It decides
105
- when an entry is taken; for the level triggers the level itself is derived from the trigger and
106
- the trade's direction, never named. The `strategy-examples` skill carries the vocabulary and the
107
- one-directional legality matrix; a CREATE without it is refused outright.
103
+ - **The `entry` axis is required on every CREATE**, all three keys, no defaults — `trigger`,
104
+ `levelOffsetAtrMultiple`, `validForBars`. Every trigger is decided at the close of the strategy's
105
+ OWN bar, so there is no confirm-timeframe key; for the level triggers the level itself is derived
106
+ from the trigger and the trade's direction, never named. A multi-bar hold belongs to the condition
107
+ that needs it (`clock: CLOSE` with its own `closes`), not to this axis. The `strategy-examples`
108
+ skill carries the vocabulary and the one-directional legality matrix; a CREATE without it is
109
+ refused outright.
108
110
  - `get_strategy_column_contract` → `outputs[].conditionOperators`. An empty array means that
109
111
  rendered header has no comparison semantics and cannot appear in a condition clause at all.
110
112
  Legality is per rendered header, not per column: a trajectory's slot header and its `_trend`
@@ -156,36 +156,34 @@ anchor, or the author gates on something that admits most bars.
156
156
 
157
157
  ## Entry
158
158
 
159
- `{ trigger, confirmTf, closes, bandAtrMultiple, levelOffsetAtrMultiple, validForBars }` — all six
160
- required on every CREATE, no defaults. This axis is replaced WHOLE on save, so an omitted key would
161
- silently revert an author's discipline rather than be refused. There is no level-source key: the
162
- level a level trigger rests at is DERIVED from the trigger and the trade's direction, never named.
163
-
164
- **The trigger decides WHEN, and for two of them WHERE, an entry is taken.**
165
-
166
- - `AT_SIGNAL` — fire the moment the radar observes the qualification flip, at whatever bar is on
167
- the tape, keeping the platform's flat wall-clock entry window. Today's behaviour.
168
- - `ON_CANDLE_CLOSE` — the flip ARMS the pair; the entry is taken only after a close on
169
- `confirmTf` that still reads the conditions true and has not displaced beyond the band.
170
- - `STOP_THROUGH_LEVEL` — a TRIGGER order rests past the Donchian channel's CURRENT edge in the
171
- trade's direction (the 20-bar high for a long, the 20-bar low for a short) by the offset, and
172
- the exchange book is the watcher; the entry is taken when price trades through, not when the
173
- platform notices.
174
- - `ON_RETEST` — a LIMIT order rests in front of the edge a close most recently BROKE (the
175
- channel's break memory: the broken high for a long, the broken low for a short) by the offset,
176
- waiting for a return to it. Not filling is a correct outcome, not a failure; no unrecovered
177
- break in memory means no setup, never a fallback level.
178
-
179
- `confirmTf` is the bar whose close confirms. Exactly two values are legal: the strategy's own
180
- timeframe and the rung below it — one only, when the strategy sits on the ladder floor. It is
181
- NOT the authorable main-candle set; a rung further down names a bar nothing else in the strategy
182
- observes and makes the radar sweep on every one of its closes.
183
-
184
- `closes` (1–5) is how many consecutive confirming closes are required, and `bandAtrMultiple` is
185
- the veto width: the entry is VOIDED when the confirming close has moved at or beyond that many
186
- ATR against the armed verdict. Strictly greater than zero — zero is not "no filter" but a filter
187
- that voids on any adverse move — and at or below the platform's own entry-deviation gate, since
188
- a wider band cannot refuse anything the platform will not refuse anyway.
159
+ `{ trigger, levelOffsetAtrMultiple, validForBars }` — all three required on every CREATE, no
160
+ defaults. This axis is replaced WHOLE on save, so an omitted key would silently revert an author's
161
+ discipline rather than be refused. There is no level-source key: the level a level trigger rests at
162
+ is DERIVED from the trigger and the trade's direction, never named. There is no confirm-timeframe
163
+ key either: the bar whose close decides an entry is the strategy's OWN timeframe.
164
+
165
+ **Every trigger is decided at the close of the strategy's own bar — there are three, and the close
166
+ is the only entry clock.** The newest settled bar is read on the closed basis — every `LIVE`-clocked
167
+ condition resolves on that bar and the scorecard reads its close — and a reading that still qualifies
168
+ fires at that close. A bar that does not qualify decides nothing and is not revisited. The fill lands
169
+ at the next tick, and the platform refuses it if the market has already run past its own drift budget
170
+ from that close. A fourth value, `AT_SIGNAL`, is readable on strategies authored before this contract
171
+ and is REFUSED on every save; there is no live-reading entry to author.
172
+
173
+ - `ON_CANDLE_CLOSE` — the entry is taken AT the qualifying close, at market.
174
+ - `STOP_THROUGH_LEVEL` — at the qualifying close a TRIGGER order rests past the Donchian channel's
175
+ CURRENT edge in the trade's direction (the 20-bar high for a long, the 20-bar low for a short) by
176
+ the offset, and the exchange book is the watcher; the entry is taken when price trades through,
177
+ not when the platform notices.
178
+ - `ON_RETEST` — at the qualifying close a LIMIT order rests in front of the edge a close most
179
+ recently BROKE (the channel's break memory: the broken high for a long, the broken low for a
180
+ short) by the offset, waiting for a return to it. Not filling is a correct outcome, not a
181
+ failure; no unrecovered break in memory means no setup, never a fallback level.
182
+
183
+ **A multi-bar hold belongs to the CONDITION that needs it.** Declare `clock: CLOSE` with that
184
+ condition's own `closes` — the entry axis counts no bars, and there is no displacement band: the
185
+ platform's entry-deviation gate measures the live mark against the decided close and refuses a fill
186
+ that drifted past the budget in either direction.
189
187
 
190
188
  `levelOffsetAtrMultiple` (0–2) is an UNSIGNED distance from the derived edge in ATR multiples — a
191
189
  long adds it, a short subtracts it, so a breakout stop rests past its edge and a pullback limit
@@ -196,11 +194,10 @@ correct side of the mark when the order is placed — a buy stop above it, a buy
196
194
  mirror for a sell — or the entry is refused rather than filled at the market; there is no limit on
197
195
  how far from the mark a level may rest.
198
196
 
199
- **The legality matrix runs one way.** All six keys are always present, so the question is never
200
- "is it set" but "is it set to something that MEANS anything under this trigger". `closes` ≠ 1 is
201
- refused under any trigger but `ON_CANDLE_CLOSE`; `levelOffsetAtrMultiple` ≠ 0 and `validForBars`
202
- ≠ 4 are refused under a non-level trigger. Leave a dial at its inert value rather than setting one
203
- the platform will ignore.
197
+ **The legality matrix runs one way.** All three keys are always present, so the question is never
198
+ "is it set" but "is it set to something that MEANS anything under this trigger".
199
+ `levelOffsetAtrMultiple` ≠ 0 and `validForBars` ≠ 4 are refused under a non-level trigger. Leave a
200
+ dial at its inert value rather than setting one the platform will ignore.
204
201
 
205
202
  ## Report sections
206
203
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: battlegrid-trade-proposal
3
- description: Find and stage a trade for one of the player's agents, for the player to approve. Activate whenever the player asks which coins fit an agent right now, wants a trade found or proposed for an agent, asks the agent to evaluate a coin, or wants to approve or decline a proposal it made. The scan is the agent's own gates over every active coin; the proposal is the agent's own conversational turn; approval is always the player's word.
3
+ description: Find and stage a trade for one of the player's agents, for the player to approve. Activate whenever the player asks which coins fit an agent right now, wants a trade found or proposed for an agent, asks the agent to evaluate a coin, or wants to approve or decline a proposal it made. The scan is the agent's own gates over every active coin; a proposal is QUEUED for the agent's next strategy-bar close and answered into the conversation when that bar is decided; approval is always the player's word.
4
4
  ---
5
5
 
6
6
  # Trade Proposal
@@ -29,33 +29,47 @@ re-sorts a list, re-derives a verdict, or approves on the player's behalf.
29
29
  - A refused scan (`RATE_LIMITED`) is a refusal: say the scan was refused and when it can be retried
30
30
  (`retryAfterSeconds`). Never say "nothing fits".
31
31
 
32
- ## 2. Propose
32
+ ## 2. Queue the request
33
33
 
34
34
  - `propose_entry_decision` **only** on the coins the player named, or on the top qualifying row
35
35
  when they asked for the best fit. One coin per call.
36
+ - **The call does not decide.** It registers a request against the agent's next strategy-bar close
37
+ and returns immediately; no model runs and nothing is spent. The agent reads that bar on its
38
+ settled close, through the same path its radar deployments use.
36
39
  - `userMessage` is the player's own words for the turn. Mint a **fresh UUID** `idempotencyKey` per
37
- proposal; a retry with the same key replays the recorded result and never runs a second turn.
38
- - The call is synchronous and may take the turn's full LLM latency. Do not retry while it is in
39
- flight; a same-key call during that time is refused with `CONFLICT`.
40
-
41
- ## 3. Report the outcome in the player's terms
42
-
43
- - `type: "recommendation"` — the PROPOSED decision, awaiting approval. State the direction, the
44
- entry, stop and take-profit levels, the position size, the **`convictionPercent`**, and the
45
- expiry (`expiresAt`). No conviction floor was applied on this surface: the conviction is the
46
- agent's own reading, and the player judges it.
47
- - `type: "no_trade"` — the reason (`reasonCode`) and the next coins worth asking about.
48
- - `type: "error"` — the engine's block or a post-billing failure, with its remedy:
49
- `OPEN_POSITION_CONFLICT` means a position already holds the slot (see step 0);
50
- `LLM_CREDITS_EXHAUSTED` means top up when `topupAvailable` is true; a `SURFACE` / `LLM_FAILURE`
51
- means the model was billed and produced nothing usable — retry **with a new key**, since the
52
- same key replays this failure.
53
- - A refused proposal (`RATE_LIMITED`) is a refusal with its retry-after. Never "nothing to do".
54
-
55
- ## 4. Approve or decline — only on the player's word
56
-
57
- - Present the decision and **ask**. `accept_entry_decision` only after the player explicitly
58
- approves; `cancel_entry_decision` only after they explicitly decline.
40
+ request; a retry with the same key replays the recorded result and never registers a second one.
41
+ - **One pending request per coin.** A second is refused with `CONFLICT` / `REQUEST_PENDING` — read
42
+ the one that exists with `get_entry_request` rather than asking again.
43
+
44
+ ## 3. Tell the player what they are waiting for
45
+
46
+ - `type: "queued"` is the normal answer. Read `request` back to them: the bar being decided
47
+ (`barStart`), when the answer is due (`decidesBy`), and the deadline past which that bar can no
48
+ longer be decided (`windowEndsAt`). `decidesBy` and `windowEndsAt` are different instants — a bar
49
+ that has already settled is decided at the next sweep, seconds away.
50
+ - Say where the answer will appear: in the agent's conversation, and — if it proposes a trade — in
51
+ `list_pending_approvals`. Nothing further is needed from the player until then.
52
+ - `get_entry_request` re-reads a request still pending; `cancel_entry_request` withdraws it. Both
53
+ are `NOT_FOUND` once it has been answered, cancelled or expired, and that is the honest record:
54
+ the answer is in the conversation.
55
+ - `type: "error"` — the block, with its remedy: `OPEN_POSITION_CONFLICT` means a position already
56
+ holds the slot (see step 0) and nothing was queued; `LLM_CREDITS_EXHAUSTED` means top up when
57
+ `topupAvailable` is true. A refused request (`RATE_LIMITED`) is a refusal with its retry-after.
58
+ Never "nothing to do".
59
+ - `type: "recommendation"` and `type: "no_trade"` arrive only as the **replay** of a request made
60
+ before the queued contract shipped. Report them as step 4 describes and do not expect them from a
61
+ fresh call.
62
+
63
+ ## 4. When the answer arrives — approve or decline only on the player's word
64
+
65
+ - A decided close produces one of: a PROPOSED decision awaiting approval; a no-trade with the
66
+ reason and the next coins worth asking about; a close that did not qualify; a window that passed
67
+ with no sweep; or a bar the agent's own radar deployment decided first and traded in full.
68
+ - On a proposal, state the direction, the entry, stop and take-profit levels, the position size,
69
+ the **`convictionPercent`**, and the expiry (`expiresAt`). No conviction floor is applied on this
70
+ surface: the conviction is the agent's own reading, and the player judges it.
71
+ - Present it and **ask**. `accept_entry_decision` only after the player explicitly approves;
72
+ `cancel_entry_decision` only after they explicitly decline.
59
73
  - Never accept because the conviction reads high, because the scan ranked the coin first, or
60
74
  because the player asked you to "find a trade" — finding is not approving. `get_entry_decision`
61
75
  re-reads the row if the conversation moved on before they answered.