Skip to content
MCP · stateless HTTP · no account

114 typed tools, one drop-in

http://127.0.0.1:8420/mcp is the same port REST and the socket answer on. Tools and REST routes are the same surface counted two ways, so an agent and a script are looking at exactly the same engine. Four lines of configuration and your model can read the market.

Tools
114

45 global · 23 × 2 · 23 venue

Transport
HTTP

stateless Streamable HTTP, tools only

Overview

Tools and REST routes are the same surface: every tool has a route behind it. What that is not is a matching count: one REST path can be many named tools, and a few capabilities are deliberately one-sided.

The registry is 45 global tools, 23 market-typed tools minted once for spot and once for futures, and 23 venue operations — 114 in total. Market-typed tools are named {spot|futures}_{tool}, so a model never has to remember to pass a market type and can never pass the wrong one.

Stateless by design. Every call stands on its own there is no session to establish, resume or lose, and nothing to clean up after a client goes away. Point as many clients at it as you like; they cannot interfere with each other.

More tools than routes. 114 tools sit over 104 routes, and the difference is almost entirely one path: POST /api/{marketType}/exchange/{method} is a single route per market type and 23 separately named tools, because a model picks a tool by name and should never have to assemble one out of a parameter.

A couple of capabilities run the other way. get_agent_capabilities mirrors GET /api/agent/capabilities exactly, and it is the one tool no narrowing can take away. delete_backtest does not exist at all: permanently removing a stored backtest report is a REST-only operation. cancel_backtest stops a running job and is idempotent, but it never removes one.

Most of the surface is modular. 45 of the 114 tools are registered by the engine’s 5 feature modules. A module that is switched off in a build does not register its tools at all, which is the same absence a narrowed scope produces. The Modules group in the contents documents them one at a time.

analyticsstrategybacktestjournalevents

Every tool carries a full description and a typed input schema, which is how a model learns the surface without you writing a prompt about it. The list your engine returns from tools/list is the authoritative one this page names the tools, and the engine ships their schemas.

Connect

Drop this into your client's MCP configuration. There is no API key to paste, no OAuth flow and no hosted endpoint the server is the engine already running on your machine.

.mcp.json
{  "mcpServers": {    "pricesylo": {      "type": "http",      "url": "http://127.0.0.1:8420/mcp"    }  }}
Claude CodeClaude DesktopCursorAny MCP client

The server key is pricesylo, the transport is http, and the path is /mcp. Anything that speaks Streamable HTTP MCP works; nothing is client-specific.

Not in the demo build. The demo serves REST and the WebSocket only. Its MCP server is never mounted and it ships no command line either, so the configuration above will simply fail to connect against a demo engine. Everything on this page needs a licensed build.

Security boundary

Local-first, not authenticatedbinds 127.0.0.1

The bind address is the security boundary

There is no account, no signup, no API key and no session. The engine is a process on your machine, and it binds to 127.0.0.1:8420. Anything that can reach the port has all 114 tools, including the 23 venue operations and the subscription tools.

Treat the loopback interface as the whole of the access-control model. Do not move the bind to 0.0.0.0, onto a LAN interface, or behind a public reverse proxy. If you need the surface from another machine, tunnel it — ssh -L rather than exposing it.

Browsers get a narrower door than curl does. A request that carries an Origin header has to name an allowed origin, so a page you happen to have open in another tab is not a working client for your engine. A request with no origin curl, a server-side job, the MCP surface is unaffected. The engine can also be started behind a shared token, in which case every request carries it as X-Engine-Token and an unauthenticated call answers 401 unauthorized.

What is never returned

Exchange credential values are masked wherever configuration is echoed, and anything an exchange sends back is scrubbed before it reaches a response, a log line or an error message. A webhook signing secret is write-only in the strictest sense: no surface returns it, nothing logs it, and a subscription record reports only whether one is set.

That is authentication, and there is deliberately none of it. Authorization is a different question and it does have an answer: scopes narrow what a client is allowed to reach, and 2 of those scopes will not act at all until the call says so explicitly. An engine on a machine you share with anyone should be narrowed by the header before it is trusted by the network.

Scopes and confirmation

The engine grants every scope by default. A client narrows its own grant it can never widen one and what it narrowed to is exactly what the tool list will show it.

Narrowing is one header. X-PS-Scopes on the POST /mcp request carries a comma-separated list of the scopes this client is willing to use. The engine intersects it with the grant the build already carries, and the intersection is final for that request. An empty list is legal, and leaves an agent with nothing but the capability probe.

A denied tool is absent, not refused. This is the difference that matters on MCP. REST answers a scope failure with a refusal you have to handle; MCP simply never registers the tool, so it does not appear in tools/list and a model never learns the name of something it is not allowed to call. Narrow the scopes and the surface shrinks in front of the model rather than failing underneath it.

The scope vocabulary

market.readcontext.readindicators.readindicators.writestructure.readevents.readstrategy.readstrategy.writebacktest.runbacktest.readjournal.readjournal.writeorders.readorders.writefavorites.writesubscriptions.writeexchange.readexchange.tradeconfig.read

2 scopes need a second signal. exchange.trade and orders.write are behind a confirmation gate. A call that needs it carries "_meta": { "autoApprove": true } in the tools/call parameters, or the request carries an X-PS-Confirmed: 1 header. Without one of the two the tool answers with a refusal instead of acting. The scope check runs first, so a scope you never asked for cannot be confirmed into existence.

In practice the gate covers the venue operations marked as moving value in the Venue operations section, plus {spot|futures}_refresh_orders and {spot|futures}_clear_order_history. No module tool is gated at all: creating an indicator, saving a strategy, queueing a backtest and writing a journal entry each need their own write scope and nothing more.

Every tool is annotated. Annotations follow the scope a tool sits on. A tool on a read scope is marked readOnlyHint ; a tool that trades, writes orders or edits the favorites list is marked destructiveHint, as is anything whose name deletes, removes, clears or cancels. A client that surfaces annotations can therefore ask a human before a destructive call without knowing anything at all about this engine.

Discovery is unscoped. get_agent_capabilities is the one tool narrowing can never remove, and it mirrors GET /api/agent/capabilities. It answers with the whole scope vocabulary and, per scope, whether this client currently holds it, whether it is gated, and which tools and routes it covers plus the confirmation contract, the socket contract, where narrowing is read from, and the modules this build loaded. An agent that has just narrowed itself calls this to find out what it has left.

curl -s -X POST http://127.0.0.1:8420/mcp \  -H 'Content-Type: application/json' \  -H 'Accept: application/json, text/event-stream' \  -H 'X-PS-Scopes: market.read,context.read,indicators.read' \  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call",       "params":{"name":"get_agent_capabilities","arguments":{}}}'
{ "jsonrpc": "2.0", "id": 5,  "result": { "content": [ { "type": "text", "text": "…" } ],              "structuredContent": { … } } }
The header narrows this request to three scopes; the answer reports what survived that narrowing, not what the build could have granted.

Handshake

Your client does this for you. It is here because being able to reproduce it with curl is the fastest way to tell a configuration problem from an engine problem.

curl -s -X POST http://127.0.0.1:8420/mcp \  -H 'Content-Type: application/json' \  -H 'Accept: application/json, text/event-stream' \  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",       "params":{"protocolVersion":"2025-06-18","capabilities":{},                 "clientInfo":{"name":"my-client","version":"1.0.0"}}}'
{ "jsonrpc": "2.0", "id": 1, "result": { … } }
Both Accept types are required by the transport, even though this server answers JSON.

Result shape

Every tool answers the same way, so a client that can read one can read all 114.

  • Text plus structure. The result carries a text content block and structuredContent. They hold the same payload the text block for a model that reads text, the structured field for a client that wants the object. A payload that is not an object is wrapped, because the protocol requires an object there.
  • Charts answer with a picture. spot_get_chart_screenshot and futures_get_chart_screenshot return an image content block — image/png, base64, no data: prefix followed by a text block carrying the same render metadata, with the image bytes stripped out of it. Read the image block for pixels and the structured field for everything else.
  • Failures are tool errors. A refusal comes back as an error result with a message, not as a transport failure. Anything an exchange echoed back is scrubbed before it reaches you.

Not implemented

This server implements tools. That is the whole surface, and the omissions are deliberate rather than pending.

CapabilityStatus
ToolsAll 114
ResourcesNone
PromptsNone
SamplingNone
SessionsStateless
AuthenticationNone loopback bind
The server declares tool capability only. A client that probes for resources or prompts is told there are none.

Tool failures

The same contract REST gives you, in the shape MCP expects.

The API is its own reference

Errors name the field and enumerate the valid set

Nothing is silently substituted, clamped or ignored. A parameter outside its allowed set is refused, and the refusal carries the offending field name followed by every value that would have worked. A typo in a timeframe, a style, a read key or an operator is therefore one request away from an answer, and you never need a hardcoded copy of an enum to write a correct client.

The same discipline applies to defaults: omit an optional parameter and the engine applies its own configured value. Send a bad one and it tells you. It will not quietly pick something near what you asked for.

One difference is worth spelling out. The engine’s 5 feature modules name their failures in upper snake case — STRATEGY_NOT_FOUND, BACKTEST_JOB_NOT_FOUND, CUSTOM_INDICATOR_INVALID while the core surface names its own in lower snake case. Both arrive the same way and both name the thing you got wrong; only the spelling differs.

A module failure can also carry more than a message. A validation refusal attaches an errors list with one entry per problem, each naming the path it sits on, and some attach a data object with the specifics. Where a refusal is expected rather than exceptional an invalid custom indicator, an invalid strategy the answer arrives as ordinary tool DATA with a false ok flag and that list, not as an error result, so a model can read the problems and try again without unwinding anything.

Core global tools

Not market-typed. Engine state, the live configuration, and the whole subscription surface a rule string carries its own market type, so a spot_ or futures_ prefix would be a second and contradictable source of truth.

ToolInputsWhat it does
engine_healthNoneLiveness and uptime. Touches nothing but the clock, so it answers even while the engine is busy
engine_statusNoneRuntime snapshot: uptime, tracked pairs, live streams, cache readiness, memory, and the effective configuration with credentials masked. Call this first to confirm the engine is up
list_platformsNoneThe eight exchanges and their cached spot and futures pair counts. The platform ids here are the only values every other tool accepts
get_configNoneThe effective configuration, credentials masked. Read-only
create_subscriptionrule, url, secret?, confirm?, cooldownMs?, rearm?, enabled?, label?Create a webhook subscription. Its description teaches the whole rule grammar with worked examples, because a model has no other way to learn it
list_subscriptionsNoneEvery subscription, enabled or not. Never fails; empty is an empty list
get_subscriptionidOne record. Poll this after creating a rule to confirm it is actually sampling
update_subscriptionid, enabledEnable or disable. enabled is the only mutable field; enabling resets the trigger state
delete_subscriptionidDelete permanently. Events it already wrote to the log survive
test_subscriptionidSend one signed test delivery now. Succeeds even when the delivery fails read the result, not the absence of an error
list_eventssince?, limit?Replay the durable event log from a cursor

The capability probe

ToolInputsWhat it does
get_agent_capabilitiesNoneGlobal, read-only and the only tool no scope can remove: it reports the scope vocabulary, which of those scopes this client currently holds, which are gated behind a confirmation, and which modules this build loaded. An agent that has narrowed itself calls this to discover what it has left
11 core tools here, plus the unscoped probe. The rest of the 45 global tools in the registry are contributed by the feature modules, which add 45 tools between them the Modules group below documents every one.

Core market-typed tools

Each name below exists twice, prefixed spot_ or futures_. The pair of names is why a model never has to reason about market type as a parameter.

Tool suffixInputsWhat it does
search_pairsplatform?, query?, limit?Search the local pair catalogue
list_favoritesNoneThe pairs this engine is actively tracking
add_favoriteplatform, symbol, timeframes?, tier?Start tracking a pair. Nothing is tracked until you ask
remove_favoriteplatform, symbolStop tracking a pair and release its streams
get_compiled_contextplatform, symbol, include?The compiled, price-stamped context for one pair. include takes one half: kline or orderflow
get_kline_contextplatform, symbol, timeframe, section?The structural analysis for one timeframe all sixteen reads, or one of them with section
get_orderflow_contextplatform, symbol, sectionOne live orderflow read
get_chart_screenshotplatform, symbol, timeframe, style?, theme?, indicators?, width?, height?, bars?, showVolume?A PNG chart, returned as an image content block. Works on any catalogued pair, favorite or not
get_klinesplatform, symbol, timeframe?, limit?, before?Raw OHLCV bars, oldest first. Also works on a pair that is not a favorite
get_recent_tradesplatform, symbol, limit?Executed prints, oldest first
get_depthplatform, symbol, levels?An order-book snapshot
get_depth_historyplatform, symbol, limit?, before?Recorded book snapshots, oldest first
get_footprintplatform, symbol, limit?, before?The one-minute bid × ask footprint, oldest first
list_ordersplatform?, symbol?, limit?Working orders the engine is tracking
get_order_historybefore?, limit?, query?Completed orders, newest first
refresh_ordersplatform, symbol?, apiKey?, apiSecret?, apiPhrase?Re-poll the venue for the current state of tracked orders. The only order tool that reaches a venue the other three read records this engine already holds
clear_order_historyNoneClear the local order history. The exchange is not touched
17 core shapes here, of the 23 market-typed shapes in the registry. With 45 global tools and 23 venue operations, that is 114 tools in all.

Context tools serve tracked pairs only, and answer thinly for the first seconds after add_favorite while the pair warms up. The two exceptions are get_chart_screenshot and get_klines: both can backfill, so both work on any catalogued pair.

Read keys

sectionpath

Read keys are enumerated by the engine, live

A pair carries sixteen structural reads per tracked timeframe and eight live orderflow reads. Each is addressable on its own, and every parameter spelled section takes one of those keys.

This page does not print the key list, because the engine already publishes it and a copy here would be the stale one. To see the keys a pair exposes right now, call the compiled-context tool, or send a deliberate typo and read the tool error. Both answers come from the running engine, so they are correct for the version you actually have.

Venue operations

Named {mt}_{operation}, one per allowlisted venue operation. One vocabulary across all eight exchanges the engine normalises what comes back, so a model does not learn eight dialects.

Real moneynever retried

Mutating tools say so, in their own description

Tools marked ⚠ act on a live account whenever credentials are present placing or cancelling orders, moving funds, or changing leverage, margin and position mode for every order that comes after and every one of them carries an explicit warning inside the description the model reads: never call this to explore or test, only on a specific instruction. A read that times out may be retried; a mutation that times out never is.

Every tool that authenticates takes apiKey, apiSecret and apiPhrase as optional inputs, overriding whatever the engine already holds for that call alone. The operations that reach no credential at all do not carry those fields, so a model cannot even offer them. Values are never echoed back and are stripped out of any error a venue returns. platform is always required.

These are also the tools behind the confirmation gate. Every operation marked below sits on the exchange.trade scope, which means a call must say so explicitly before it will act — the flag and the header are documented under Scopes. Drop that scope from the header and the whole marked half of this list disappears from the tool list.

spot_ 7 tools

spot_getUserWalletBalanceInfospot_getTradMarketInformationspot_getTradeOrderInformationspot_extractTradeInfospot_createMarketTradeOrderBx moves valuespot_cancelMarketTradeOrderBx moves valuespot_transferUsertradeProfits moves value

futures_ 16 tools

futures_getUserWalletBalanceInfofutures_getMarketContractPositionfutures_getContractOrderInformationfutures_getMarketContractTradeStatefutures_getContractMarketInformationfutures_getContractMarketOpenInterestfutures_extractTradeInfofutures_createMarketContractOrder moves valuefutures_createBracketContractOrder moves valuefutures_closeMarketContractPosition moves valuefutures_cancelMarketContractOrder moves valuefutures_cancelAllMarketContractOrder moves valuefutures_setMarketContractLeverage moves valuefutures_setMarketContractMarginType moves valuefutures_setMarketContractPositionMode moves valuefutures_transferUsertradeProfits moves value

Parameter names are identical to the REST passthrough — the full parameter list is on the REST page. A dotted chip marks an operation that moves value.

Indicator tools

Read the built-in indicator catalogue, compute any of it over a pair, run the engine’s structural and orderflow analysis, and write indicators of your own in the engine’s formula language.

33 indicators ship built in, filed under 7 categories. Anything you write yourself lands in the first of them and is indistinguishable from the rest afterwards: it lists, it computes by id, and the strategy validator resolves it.

custommomentumsessionstructuretrendvolatilityvolume
ToolInputsScopeWhat it does
list_indicatorsquery?, category?, ids?indicators.readThe catalogue, filterable by free text, by category, or by an explicit list of ids. Indicators you wrote appear here too, under the custom category
get_indicatoridindicators.readOne catalogue entry, including its parameters and what each of its outputs means
get_custom_indicator_guideNoneindicators.readThe whole formula language in one call: the grammar, the bar fields, the series functions, the callable built-ins, worked examples and the hard limits. A model reads this before it writes its first definition
list_custom_indicatorsNoneindicators.readEvery custom indicator this engine has stored
get_custom_indicatoridindicators.readOne stored definition
validate_custom_indicatordefinitionindicators.readParse and type-check a definition without storing it. Answers with the normalised definition and one entry per problem, each naming the path it sits on
create_custom_indicatordefinitionindicators.writewritesStore a definition. The id must start cx_ and is derived from the name when you leave it out
update_custom_indicatorid, definitionindicators.writewritesMerge changes into a stored definition and bump its version. Parameters and outputs are replaced whole rather than merged into
delete_custom_indicatoridindicators.writewritesRemove a stored definition. Refused while a saved strategy still references it
{spot|futures}_preview_custom_indicatorsymbol, platform?, timeframe, bars?, definition, params?indicators.readCompute an unsaved definition against a tracked pair and hand back a per-output summary. This is the step between validating a definition and committing to it
{spot|futures}_compute_indicatorssymbol, platform?, timeframe, limit?, indicatorsindicators.readCompute a batch of indicators over one pair in a single pass, on one shared time axis. An entry may name a higher timeframe than the request and is aligned down to it; a lower one is refused
{spot|futures}_get_structuresymbol, platform?, timeframe, limit?, left?, right?, lookback?, atrMult?, atrPeriod?, tolerancePct?structure.readThe structural analysis for one timeframe, or one read of it by name. The engine enumerates the reads it exposes
{spot|futures}_get_orderflow_metricssymbol, platform?, levels?, obiDepth?structure.readLive book-derived metrics for one pair. A metric the book cannot support yet comes back marked unavailable with a reason, rather than as a number you should not have trusted
A name written {spot|futures}_ is minted twice, once per market type, and the count above counts both copies.

The formula language. 41 series functions run over 10 bar fields, and all 33 built-ins are callable from inside a formula. A formula is never evaluated as source, and every one of them is bounded: no loops, no recursion, constant windows, and a null that propagates rather than a number that lies. Ids are namespaced cx_, and one custom indicator cannot call another.

The lifecycle is guide, validate, preview, store. get_custom_indicator_guide teaches the language, validate_custom_indicator type-checks a draft, {spot|futures}_preview_custom_indicator runs it against a real pair so you can look at the numbers, and create_custom_indicator stores it. Skipping the middle two is allowed and is how most first attempts fail.

Invalid definitions come back as data. This is deliberate and it is the one place this server bends the usual rule. Creating or updating an indicator the engine cannot compile answers with a normal result carrying a false ok flag and an errors list, not with a tool error so a model reads what was wrong, fixes it and calls again inside the same turn.

curl -s -X POST http://127.0.0.1:8420/mcp \  -H 'Content-Type: application/json' \  -H 'Accept: application/json, text/event-stream' \  -H 'X-PS-Scopes: indicators.read,indicators.write' \  -d '{"jsonrpc":"2.0","id":7,"method":"tools/call",       "params":{"name":"create_custom_indicator","arguments":{"definition":{         "id":"cx_trend_pressure",         "name":"Trend Pressure",         "params":[{"key":"len","type":"int","default":20,"min":2,"max":200}],         "outputs":[           {"key":"bias","formula":"ema(close, len) - ema(close, len * 3)",            "kind":"line","pane":"sub","anchor":"none","side":"neutral"},           {"key":"hot","formula":"bias > 0 and rsi(14) > 60",            "kind":"marker","pane":"main","anchor":"high","side":"bull"}],         "tags":["trend"]}}}}'
{ "jsonrpc": "2.0", "id": 7,  "result": { "content": [ { "type": "text", "text": "…" } ],              "structuredContent": { "ok": false, "errors": [ … ] } } }
The result shown is the failure shape: a refusal arrives as data, one entry per problem. A definition that compiles answers with the stored version instead.

Strategy tools

A strategy is a declarative document market, inputs, entry, filters, exit, risk that the engine validates, versions, and can explain back to you in prose. Every write produces a new version; nothing is ever edited in place.

ToolInputsScopeWhat it does
list_strategiesNonestrategy.readEvery saved strategy, with its current version and the list of versions behind it
get_strategyid, version?strategy.readOne strategy, at its current version or at any earlier one
create_strategydefinitionstrategy.writewritesSave a definition as its first version. An invalid definition is refused, and the refusal names every path that failed
refine_strategyid, ops, changeSummary?strategy.writewritesApply a list of operations to a saved strategy. The result is a new version that records its parent, what changed, and a summary the engine generates for you
validate_strategydefinitionstrategy.readCheck a draft without saving it: errors, warnings, the indicator references it resolves to, and how many bars it needs before it can trade
delete_strategyidstrategy.writewritesRemove a strategy and every version of it
get_strategy_schemasection?, withExamples?strategy.readThe definition schema, one section at a time, so an author is not handed the whole document before writing a single entry condition
diff_strategy_versionsid, from?, to?strategy.readWhat changed between two versions
explain_strategyid, version?strategy.readA saved strategy, described in prose
explain_strategy_definitiondefinitionstrategy.readThe same explanation for a draft that has never been saved
None of these are market-typed: one tool answers whichever market you are looking at.

Refinement is a list of operations, not a rewrite. Each one names a dotted path into the definition and what to do to it, which is what lets a model change a risk floor without restating a strategy it might get wrong. refine_strategy accepts these verbs:

setunsetpushremove
refine_strategy
curl -s -X POST http://127.0.0.1:8420/mcp \  -H 'Content-Type: application/json' \  -H 'Accept: application/json, text/event-stream' \  -H 'X-PS-Scopes: strategy.read,strategy.write' \  -d '{"jsonrpc":"2.0","id":9,"method":"tools/call",       "params":{"name":"refine_strategy",                 "arguments":{"id":"breakout_1h",                              "ops":[{"op":"set","path":"risk.minRR","value":2},                                     {"op":"unset","path":"risk.maxDailyLoss"}],                              "changeSummary":"raise the reward floor"}}}'
Two operations, one new version. The parent version, the changed paths and a generated summary are recorded for you.

Backtest tools

Backtests are jobs, not calls. Starting one answers immediately with a job id; you either poll it or subscribe to the backtest channel on the socket and be told.

ToolInputsScopeWhat it does
{spot|futures}_run_backteststrategyId?, version?, definition?, symbol?, platform?, timeframe?, range?, fees?, slippageBps?, initialEquity?, quoteCurrency?backtest.runwritesQueue a backtest of a saved strategy or of an inline definition, over a bar range, with the fees, slippage and starting equity you choose. Answers with a job id and a queued status, never with a report
get_backtestjobIdbacktest.readThe job, and its report once the job is done. A report that is no longer available comes back as a warning rather than as a job that vanished
list_backtestsstrategyId?, status?, limit?backtest.readJobs, filterable by strategy and by status
cancel_backtestjobIdbacktest.runwritesAsk a job to stop. Idempotent: cancelling one that already finished is not an error, it simply reports that nothing was cancelled
compare_backtestsjobIdsbacktest.readTwo job ids, scored side by side
score_tradestrades, equityCurve?, initialEquity?, quoteCurrency?backtest.readRun the same scoring the backtester uses over a list of trades you supply. Nothing is stored
A name written {spot|futures}_ is minted twice, once per market type, and the count above counts both copies.

Job statuses. The last three are terminal

queuedrunningdonefailedcancelled

There is no delete tool. delete_backtest is not part of this surface: permanently removing a stored job and its report is a REST-only operation, because it is the one thing on this module an agent should not be able to do on its own. cancel_backtest stops a job and can be called as many times as you like, but it never removes one.

Only one code means the job is gone. BACKTEST_JOB_NOT_FOUND A job that failed, was cancelled or whose report is no longer available still answers. Treat anything else as a job that still exists.

curl -s -X POST http://127.0.0.1:8420/mcp \  -H 'Content-Type: application/json' \  -H 'Accept: application/json, text/event-stream' \  -H 'X-PS-Scopes: backtest.run,backtest.read' \  -d '{"jsonrpc":"2.0","id":10,"method":"tools/call",       "params":{"name":"spot_run_backtest",                 "arguments":{"strategyId":"breakout_1h",                              "platform":"binance","symbol":"BTCUSDT","timeframe":"1h",                              "range":{"bars":2000},                              "fees":{"takerPct":0.04},"slippageBps":1,                              "initialEquity":10000}}}'
{ "jsonrpc": "2.0", "id": 10,  "result": { "content": [ { "type": "text", "text": "…" } ],              "structuredContent": { "jobId": "…", "status": "queued" } } }
The queue answers instantly. The bar range, the fee percentage, the slippage and the starting equity are all yours to set; every one of them has a default.

Journal tools

The journal records trades and what you were thinking when you took them. Entries the engine captured from your own order stream sit beside entries you wrote by hand, and both are scored the same way.

ToolInputsScopeWhat it does
list_journal_entriesfrom?, to?, symbol?, platform?, marketType?, status?, origin?, direction?, strategyId?, setup?, tag?, offset?, limit?journal.readEntries, filtered and paged. Every filter is optional and they compose
get_journal_entryidjournal.readOne entry
create_journal_entryentryjournal.writewritesWrite an entry by hand, for a trade placed somewhere this engine never saw
update_journal_entryid, patchjournal.writewritesPatch the parts of an entry that are opinion rather than fact. An attachments patch replaces the list rather than adding to it
delete_journal_entryidjournal.writewritesRemove an entry
get_journal_statsfrom?, to?, symbol?, …, initialEquity?journal.readAggregate performance over any filtered slice, alongside a behaviour block covering how you traded rather than only what it returned
get_journal_calendarmonth, timezoneOffsetMinutes?, from?, to?, symbol?, …journal.readA dense month grid. A trade lands on the day it closed, in UTC unless you pass an offset
derive_trade_outcomesrecordsjournal.readPair arbitrary order records into round trips and score them, without writing anything to the store. Useful for records this engine never held
None of these are market-typed: one tool answers whichever market you are looking at.

Do not write what was already captured. captured entries derive themselves from the engine’s own order stream and are idempotent; manual entries are for trades placed somewhere else. Writing an entry for a trade this engine watched gives you the same trade twice, and every statistic downstream believes both.

Identity cannot be patched. The symbol, the direction and the entry facts are what an entry IS: to change one of them, delete the entry and write it again. Everything else the notes, the tags, the grades, the mood, the reflection is patchable for as long as you keep the entry. Attachments carry a reference, never bytes.

create_journal_entry
curl -s -X POST http://127.0.0.1:8420/mcp \  -H 'Content-Type: application/json' \  -H 'Accept: application/json, text/event-stream' \  -H 'X-PS-Scopes: journal.read,journal.write' \  -d '{"jsonrpc":"2.0","id":12,"method":"tools/call",       "params":{"name":"create_journal_entry","arguments":{"entry":{         "platform":"binance","marketType":"spot","symbol":"BTCUSDT",         "direction":"long","status":"closed",         "entryTime":"2026-09-08T09:15:00Z","entryPrice":61250,         "exitTime":"2026-09-08T11:40:00Z","exitPrice":62180,         "qty":0.15,"plannedStop":60800,         "setup":"range break","tags":["london"],"mood":"calm"}}}}'
A closed long, written by hand. The planned stop is what makes an R multiple computable; leave it out and the entry still scores, just without one.

Market-event tools

The engine watches the tape and the book for named patterns and scores each one it finds. Nothing here is simulated or backfilled: an event exists because it happened while the engine was watching.

ToolInputsScopeWhat it does
{spot|futures}_get_market_eventssymbol?, platform?, since?, until?, limit?, types?, side?, urgency?, minScore?events.readScored events for one pair or for a whole market type, filtered by type, side, urgency, score floor and time window
list_market_event_typesNoneevents.readThe event vocabulary and the score thresholds that separate low, medium and high urgency
A name written {spot|futures}_ is minted twice, once per market type, and the count above counts both copies.

16 event types

large_printblock_tradeliquidity_sweepunusual_volumebuy_pressuresell_pressurespoofingicebergwall_mountedwall_pulledliquidity_vacuumimbalance_clusterabsorptionboschochdisplacement

Every event carries a score. It runs from 0 to 100, and it is what urgency is derived from. Filter on urgency for a human-sized feed and on minScore when you want a hard floor. The thresholds are published by list_market_event_types, so a client never has to hard-code them.

Repeats are updates, not duplicates. An event that keeps happening at the same price is re-scored and re-emitted rather than appended, and it says how many times it has been seen and over what window. For a live feed rather than a query, the socket carries a market-events channel.

spot_get_market_events
curl -s -X POST http://127.0.0.1:8420/mcp \  -H 'Content-Type: application/json' \  -H 'Accept: application/json, text/event-stream' \  -H 'X-PS-Scopes: events.read' \  -d '{"jsonrpc":"2.0","id":13,"method":"tools/call",       "params":{"name":"spot_get_market_events",                 "arguments":{"platform":"binance","symbol":"BTCUSDT",                              "types":"absorption,liquidity_sweep,bos",                              "urgency":"high","minScore":75,"limit":50}}}'
Three types, high urgency only, with a score floor. Every filter is optional; without them you get the most recent events for the pair.

The seven subscription tools

A subscription is one predicate evaluated against live market data plus an HTTP endpoint the engine POSTs to when it fires. This is how a model arranges to be told about something later.

create_subscription, list_subscriptions, get_subscription, update_subscription, delete_subscription, test_subscription and list_events are the same code path as the REST subscription routes, and they return the same records.

enabled is the only mutable field: changing a rule means delete and create. test_subscription proves an endpoint is reachable without waiting for a rule to fire, and it succeeds even when the delivery fails read the result rather than the absence of an error.

Creating, updating and deleting a subscription all sit on the subscriptions.write scope, and none of them is behind the confirmation gate: a rule that fires only sends an HTTP request to an endpoint you nominated yourself.

Engine codeHTTPEnvelope code
invalid_request400validation_error
not_found404not_found
limit_reached429too_many_requests
forbidden_url403forbidden_url
duplicate409duplicate
disabled503service_unavailable
MCP renders the same token in front of the message that REST puts in the envelope.

Rule grammar

Exactly one predicate per rule there is no AND, OR or NOT. The same string is accepted verbatim over REST.

rule
<platform>:<marketType>:<symbol>:<timeframe>:<path> <operator> <value> binance : spot : BTCUSDT : 30m : <path>  crosses_above  68000   |        |        |      |       |          |          |platform    |     symbol    |     path     operator   threshold       marketType       timeframe

<path> addresses a value inside the pair’s published context. Call spot_get_compiled_context to see exactly which paths a pair currently exposes, then write the dotted route to the value you care about. Paths are case-sensitive, and the timeframe field is always required because it routes the rule into the evaluation index.

OperatorValueSemantics
> < >= <=numberTrue for as long as the level comparison holds
== !=number or stringStrict and type-aware no coercion, so the string "46" never equals the number 46. Strings compare case-sensitively after trim
crosses_abovenumberFires on the transition upward through the threshold, not while above it
crosses_belownumberFires on the transition downward through the threshold
entersstringFires on the transition into that state
exitsstringFires on the transition out of that state
Ten operators. A string threshold is quoted; an unquoted numeric literal is a number.
Before you trust a rule

A stored rule is not necessarily a sampling rule

The pair has to be tracked. Creating a subscription does not start tracking a pair that would change the engine’s workload without being asked. A rule addressed at a pair you have not favorited, or at a timeframe that favorite does not track, is accepted and stored and then stays inert. Favorite the pair first.

Level rules and edge rules behave differently. > is true for as long as the value stays above the threshold, so it fires immediately if the condition is already true when you create it. crosses_above needs a previous sample below and a current sample at or above, so it never fires on the first observation. Read them as “tell me while it is above” versus “tell me when it goes above”.

Confirmation, cooldown and hysteresis are always on. They have defaults and you may tune them per subscription, but there is no way to switch any of them off. One noisy tick cannot alert you, a permanently true condition cannot alert you repeatedly, and a value hovering on a threshold has to travel back through a re-arm band before the rule may fire again.

Four surfaces, one port

Coverage is deliberately uneven, and flattening it would be the easiest thing on this page to get wrong. Market context is on all four. Chart rendering is on three a terminal client has nowhere to put a PNG. Webhook subscriptions are on two, REST and MCP, by decision: a socket you have to keep open is not what a webhook is for.

http://127.0.0.1:8420/mcp

MCP stateless Streamable HTTP, 114 typed tools at 1-to-1 REST parity.

You are here

node server/cli.js

CLI a terminal client over REST and /stream. 11 commands, payload on stdout.

Ships with the engine