DMTrading Rules API
Write alerting rules as JSON, submit them with an API key, and we run them against exchange and on-chain market data and tell you when they fire — with a stamped record of what happened next.
You are probably here to hand this page to your own assistant. That is the
intended way to use it. The machine-readable contract is /client/api/openapi.json; the DSL vocabulary
and JSON Schema are at /client/api/vocabulary; and there is a
copy-pasteable prompt at the bottom of this page.
Assistant workflow, market reads and baskets
Start with get_capabilities. Resolve markets, preflight the rule,
create when authorized, then read back its status. Valid JSON is not a ready alert.
Candle-read intervals are separate from signal evaluation timeframes.
basket.v1 applies one setup/timeframe to a fixed symbols list or
a top-N spot volume universe. Each active or paused member uses one signal slot.
Top-N membership refreshes after catalog sync; removed members retain their history.
One-shot applies per member. Use the returned member IDs to read individual fire histories.
-
GET /client/api/capabilities·get_capabilities - Discover rule families, offered venues and catalog freshness, separate read/evaluation timeframes, account quota and delivery readiness. Start here; offered does not mean ready. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/markets·list_markets - Find spot symbols. volume_24h_usd returns the curated top-20 USD-equivalent volume ranking (not order-book liquidity). sort=symbol searches all online instruments. Missing/stale catalogs are explicit. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/ohlcv·get_ohlcv - Read up to 300 recent spot OHLCV bars from the exchange/chart cache, with decimal strings, closed/forming flags, gaps and freshness. Read intervals do not imply signal evaluation support. Upstream failures are unknown, never zero. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/ohlcv/batch·get_ohlcv_batch - Read up to ten spot markets in one bounded call. source=market returns exchange chart bars; source=engine returns the stored successor-confirmed rollups used by signal evaluation. Each symbol has its own success, invalid or unavailable result. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/derivatives/series·get_derivatives_series - Read up to 300 stored public Bybit derivatives observations aligned to a requested grain. Values are exact decimal strings; missing and stale inputs carry blockers and never become zero. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/derivatives/snapshot·get_derivatives_snapshot - Read a bounded public Bybit funding, open-interest, USD open-interest and perp/spot-basis snapshot for up to 20 explicitly named spot symbols. Stale fields are null with blockers. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/levels·get_market_levels - Calculate exact prior-range, confirmed swing and rolling VWAP levels from the engine's successor-confirmed closed bars. The response pins the calculation version, UTC session, lookback inclusion policy and missing-input blockers. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/assistant/context·get_assistant_context - Read one bounded snapshot containing quota, safe delivery readiness, a selected venue's top markets, owned rules and baskets, active preparation blockers, and recent fires. It contains no channel destinations or key secrets. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/onchain/assets/{id}·get_onchain_asset - Read one on-chain asset, its canonical pool, freshness and the exact current/previous watcher inputs plus successor-confirmed hourly candles. Missing fields remain null and are listed as unknown. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/markets/prepare·prepare_market - Queue a bounded, deduplicated history preparation for one listed market, setup and signal timeframe. Poll the returned preparation until ready or blocked; provider failures remain explicit and retryable. Required scopes: rules:write, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/market-preparations/{id}·get_market_preparation - Read one owned market-preparation request with requested depth, stored coverage, gaps, the latest engine-confirmed bar, progress and a retryable blocker. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/preflight·preflight_rule - Check a watcher.v1 or signal.v1 rule plus delivery settings without saving. Returns valid and ready separately, canonical rule, quota, data, delivery and halt blockers. Readiness is a snapshot, not a reservation. Requires rules:read and market:read. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/price-alerts·create_price_alert - Create an observed-tick exact Decimal price crossing or percent-move alert on one listed spot market. A crossing uses persisted before/after observations; missing or stale prices are reported as unknown. Use condition.v1 close_above, close_below, enter or leave for successor-confirmed closed-bar alerts. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/price-alerts·list_price_alerts - List owned price alerts with exact thresholds, revisions and evaluation health. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/price-alerts/{id}·get_price_alert - Read one owned price alert. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
PATCH /client/api/price-alerts/{id}·update_price_alert - Patch one owned price alert. expected_revision is required so concurrent logic or routing edits cannot overwrite each other. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/price-alerts/{id}/pause·pause_price_alert - Pause evaluation of one owned price alert. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/price-alerts/{id}/resume·resume_price_alert - Resume one owned price alert; the next fresh observation seeds a new crossing baseline. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
DELETE /client/api/price-alerts/{id}·delete_price_alert - Delete one owned price alert and its fire history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/price-alerts/{id}/fires·get_price_alert_fires - Read bounded fire history with exact observed prices and correlated delivery state. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/condition-alerts/preflight·preflight_condition_alert - Validate one condition.v1 document and report closed-bar data, delivery, quota and halt blockers without saving. It supports exact close_above/close_below, inclusive enter/leave zones and persisted sweep_reclaim sequences. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/condition-alerts·create_condition_alert - Save a condition.v1 document. A ready document becomes active; a valid blocked document is saved explicitly as a draft with its blockers. Required scopes: rules:write, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/condition-alerts·list_condition_alerts - List owned advanced conditions with revisions, persisted sequence state and evaluation health. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/condition-alerts/{id}·get_condition_alert - Read one owned advanced condition. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
PATCH /client/api/condition-alerts/{id}·update_condition_alert - Replace one condition document and settings with an expected revision. Logic changes create a new immutable revision and reset sequence state. Required scopes: rules:write, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/condition-alerts/{id}/pause·pause_condition_alert - Pause closed-bar evaluation while preserving state and history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/condition-alerts/{id}/resume·resume_condition_alert - Recheck readiness and resume; unresolved blockers leave an explicit draft. Required scopes: rules:write, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
DELETE /client/api/condition-alerts/{id}·delete_condition_alert - Delete one owned advanced condition and its version/fire history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/condition-alerts/{id}/fires·get_condition_alert_fires - Read observed fires with exact price, closed-bar trace and correlated delivery state. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/condition-alerts/{id}/versions·get_condition_alert_versions - Read immutable condition logic revisions. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/condition-alerts/replay·replay_condition_alert - Evaluate condition.v1 with the live closed-bar engine and return bounded historical example fires without arming or mixing them into observed statistics. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/signal-baskets/preflight·preflight_signal_basket - Resolve and check a basket.v1 setup across fixed symbols or dynamic top_n volume markets. Reports the separate basket and active-member evaluation budgets before creation; saves nothing. Requires rules:read and market:read. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/signal-baskets·create_signal_basket - Create a basket.v1 atomically after preflight. It uses one basket allowance plus one active-member evaluation unit per selected market; standalone signal slots are separate. top_n refreshes on successful catalog sync; symbols is fixed. one_shot applies per member. Requires rules:write, rules:read and market:read. Required scopes: rules:write, rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/signal-baskets·list_signal_baskets - List your signal baskets, desired membership, member rule IDs, readiness and retained history. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
GET /client/api/signal-baskets/{id}·get_signal_basket - Read one owned basket and its members. Member fire histories use get_signal_alert_fires with the returned rule IDs. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
PATCH /client/api/signal-baskets/{id}·update_signal_basket - Patch one or more fields on an owned basket after preflight. expected_revision is required; stale edits return the current revision. Omitted fields retain their stored values, plan:null clears a plan, and a complete definition remains valid. Membership and delivery changes activate atomically and child alerts cannot be edited independently. Required scopes: rules:write, rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/signal-baskets/{id}/pause·pause_signal_basket - Pause a basket and its active members; retain membership, quota and history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
POST /client/api/signal-baskets/{id}/resume·resume_signal_basket - Resume a basket, retry membership readiness and preserve per-market one-shot/hysteresis state. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
-
DELETE /client/api/signal-baskets/{id}·delete_signal_basket - Delete your basket, its member alerts and their fire history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
Two kinds of rule
There are two document classes, and which one you want follows from what you are watching.
-
A coin watcher
—
watcher.v1, at/client/api/rules— is a condition tree over on-chain coins it finds for itself: "any Solana coin over $100k liquidity that jumps 15% in an hour". You do not name the coin; the rule discovers it. -
A signal alert
—
signal.v1, at/client/api/signal-alerts— names one exchange-listed market and one classical technical setup on one timeframe: "RSI crosses into oversold on the daily, SOL-USD on Coinbase". The setups are a curated closed list, not a formula builder.
The two version independently, because the version string pins a table and the
two classes have nothing in their tables in common. Everything else is shared:
the same key, the same rules:read
/ rules:write
scopes, the same response envelope, the same structured errors and the same
tier limits. Learn this surface once and you can write either.
Getting a key
Sign in with your Solana wallet, open API keys, and create one. The plaintext is shown exactly once and only its hash is stored, so if you lose it, rotate rather than recover. Send it as a bearer token:
curl -H "Authorization: Bearer dmt_live_…" \
https://app.dutchmantrading.com/client/api/vocabulary
A key carries explicit scopes, and an endpoint that needs one it lacks
answers 403 naming the scope:
| Scope | Grants |
|---|---|
| rules:read | Read your rules, their fires, and their stamped track records |
| rules:write | Create, update, pause and delete your rules |
| market:read | Read asset, pool, liquidity and candle data |
How much you can run is sized by the NSF you hold in that wallet —
nothing is spent or deducted. /client/api/usage reports the tier, the
limits and how much of them is in use.
Endpoints
| Method | Path | Scope | What it does |
|---|---|---|---|
| GET | /client/api/vocabulary | — | The field table, grammar, limits and JSON Schema. |
| POST | /client/api/rules/validate | rules:read | Check a document. Stores nothing; returns every error at once. |
| GET | /client/api/rules | rules:read | Your rules, newest first, each with its summary and track record. |
| POST | /client/api/rules | rules:write | Store a rule and start evaluating it. |
| GET | /client/api/rules/{id} | rules:read | One rule. |
| PATCH | /client/api/rules/{id} | rules:write | Change a rule. The document is re-validated only when it changes. |
| DELETE | /client/api/rules/{id} | rules:write | Delete a rule and its fire history. Permanent. |
| POST | /client/api/rules/{id}/pause | rules:write | Stop evaluating it. Still occupies a tier slot. |
| POST | /client/api/rules/{id}/resume | rules:write | Evaluate it again and clear the gate reason. |
| GET | /client/api/rules/{id}/fires | rules:read | What it reported, with the +1h / +24h outcomes stamped on each. |
| GET | /client/api/signal-alerts/schema | market:read | The setup catalog: every setup, its timeframes, its warm-up and what it means. |
| GET | /client/api/signal-alerts/availability | market:read | Whether a market can carry a setup today, and where it cannot, why. |
| POST | /client/api/signal-alerts/validate | rules:read | Check a signal-alert document. Stores nothing. |
| GET | /client/api/signal-alerts | rules:read | Your signal alerts, newest first. |
| POST | /client/api/signal-alerts | rules:write | Store a signal alert and start evaluating it. |
| GET | /client/api/signal-alerts/{id} | rules:read | One signal alert. |
| PATCH | /client/api/signal-alerts/{id} | rules:write | Change one. A partial document is merged onto the stored one and re-checked. |
| DELETE | /client/api/signal-alerts/{id} | rules:write | Delete it and its fire history. Permanent. |
| POST | /client/api/signal-alerts/{id}/pause | rules:write | Stop evaluating it. Still occupies a tier slot. |
| POST | /client/api/signal-alerts/{id}/resume | rules:write | Evaluate it again, re-armed, and clear the gate reason. |
| GET | /client/api/signal-alerts/{id}/fires | rules:read | What it reported, with the +1h / +24h outcomes stamped on each. |
| GET | /client/api/usage | rules:read | Tier, limits, what is in use, and this key's counters. |
| POST | /client/api/mcp | per tool | Model Context Protocol endpoint carrying the same operations. |
The coin-watcher document
Every rule declares the vocabulary version it was written against — currently watcher.v1. That string is a contract: a stored rule keeps meaning
what it meant when it was accepted, so the field table changing is a new
version rather than an edit in place.
A document has up to five keys. universe
chooses which coins are
considered at all; when
is the condition tested against each of
them. name
and note
are the only free text, capped at 80 and 500 characters and scrubbed of control and
invisible characters before storage.
POST /client/api/rules
Authorization: Bearer dmt_live_…
Content-Type: application/json
{
"rule": {
"dsl_version": "watcher.v1",
"name": "Solana trenchers",
"note": "the 100k / 5 day screen",
"universe": {"chain": "solana", "gate_passed": true},
"when": {"all": [
{"above": {"left": "liquidity_usd", "right": {"value": 100, "unit": "k"}}},
{"below": {"left": "pool_age_hours", "right": {"value": 5, "unit": "days"}}},
{"above": {"left": "volume_24h_usd", "right": {"value": 250, "unit": "k"}}},
{"cross_above": {"left": "price_change_1h_pct", "right": 15}}
]}
},
"once_per_asset": true,
"cooldown_seconds": 3600
}
"Tell me when a Solana coin that has cleared the gate holds $100k liquidity
inside its first five days, is actually trading, and jumps 15% in an hour."
Every field in it is one this deployment computes right now, so this is the
example to copy. Thresholds take a unit object, so 5 days
is exact rather than a hand-computed 120.
cross_above compares this bar against the previous one, so the
last condition fires on the bar the move crosses 15% rather than on every bar
it stays above it. Cross operators are only offered on fields the engine
carries a previous-bar value for — the table below says which.
The coin-watcher vocabulary
Closed and generated from the validator, so this table is what is actually
enforced. A field that is not here is an error, never a silently dropped
condition. universe
fields select the coin set; condition
fields are measured per coin and compared
inside when.
| Field | Scope | Type | Unit | Operators | Range | Available | What it measures |
|---|---|---|---|---|---|---|---|
| chain | universe | enum | — | is, one_of | base, robinhood, solana | yes | Which chain the coin trades on. Give one chain as a string or several as an array. Naming a chain is the single largest reduction in a rule's evaluation cost; omitting it scans every chain. |
| trending | universe | boolean | — | is | true, false | yes | True while the coin appears on a data aggregator's trending list. This is a statement about that aggregator's output, computed by methods it does not publish, not an independent measurement of the market. |
| gate_passed | universe | boolean | — | is | true, false | yes | True once the coin has cleared the mechanical liquidity, age and staleness gate. A coin we have not checked yet reads as false, never as true. |
| liquidity_usd | condition | number | usd (b, k, m, usd) | above, below, between | 0 to 1,000,000,000,000 | yes | US-dollar reserve held in the coin's canonical pool. Has no previous-bar value, so it takes no cross operator; pair "above" with once_per_asset to fire the first time a coin reaches a level. |
| pool_age_hours | condition | number | hours (days, hours, minutes, weeks) | above, below, between, cross_above | 0 to 87,600 | yes | Hours since the pool was created, falling back to when we first saw the coin. Accepts a unit object such as {"value": 5, "unit": "days"}. Age only increases, so cross_above fires once when a pool comes of age and cross_below can never be true. |
| volume_24h_usd | condition | number | usd (b, k, m, usd) | above, below, between | 0 to 1,000,000,000,000 | yes | US-dollar volume traded through the canonical pool over the trailing 24 hours, as reported by the pool. No previous-bar value, so no cross operator. |
| price_usd | condition | number | usd (b, k, m, usd) | above, below, between | 0 to 1,000,000,000 | yes | Latest US-dollar price from the canonical pool, falling back to the last confirmed hourly close. No previous-bar value, so no cross operator; use a price_change field to talk about movement. |
| price_change_1h_pct | condition | number | percent | above, below, between, cross_above, cross_below | -100 to 10,000,000 | yes | Percentage change in close price over the last hour. Negative for a fall; cannot go below -100. |
| price_change_6h_pct | condition | number | percent | above, below, between, cross_above, cross_below | -100 to 10,000,000 | yes | Percentage change in close price over the last six hours. Negative for a fall; cannot go below -100. |
| price_change_24h_pct | condition | number | percent | above, below, between, cross_above, cross_below | -100 to 10,000,000 | yes | Percentage change in close price over the last 24 hours. Negative for a fall; cannot go below -100. |
| attention_mentions | condition | integer | count | above, below, between, cross_above, cross_below | 0 to 100,000,000 | no |
Raw count of posts mentioning the coin on X in the most recent complete hourly sample. A raised count means people are posting; coordinated posting is indistinguishable from organic posting at this resolution.
Not available: No X credential is configured on this deployment, so no attention samples are being collected and this field is nil for every coin. A rule using it is accepted and stored, and holds on every pass until the source is connected — at which point it starts firing with no change to this contract and no new dsl_version.
|
| attention_z | condition | number | sigma | above, below, between, cross_above, cross_below | -1,000 to 1,000 | no |
Standard deviations the latest mention count sits above the coin's own 24-hour baseline, so a quiet ticker and a loud one are measured on the same scale. Needs a baseline: a coin sampled for the first time has no z-score.
Not available: No X credential is configured on this deployment, so no attention samples are being collected and this field is nil for every coin. A rule using it is accepted and stored, and holds on every pass until the source is connected — at which point it starts firing with no change to this contract and no new dsl_version.
|
| attention_ratio | condition | number | ratio (ratio, x) | above, below, between, cross_above, cross_below | 0 to 100,000 | no |
Latest mention count divided by the coin's own 24-hour baseline mean, so 3 means three times its usual chatter. Needs the same baseline as attention_z.
Not available: No X credential is configured on this deployment, so no attention samples are being collected and this field is nil for every coin. A rule using it is accepted and stored, and holds on every pass until the source is connected — at which point it starts firing with no change to this contract and no new dsl_version.
|
above and below are strict; between is
inclusive on both bounds. A rule may nest 6 levels deep, hold 32 comparisons, and put 16 nodes in a single
group.
Published is not the same as available
The Available
column is about this deployment, not about the
DSL version. A field marked no
is part of watcher.v1
and is accepted by the validator — but nothing is producing a value for it, so
a rule that uses it stores and then holds on every pass instead of firing. The
flag is read when this page is rendered, so it changes on its own when a
source is connected, without a new version and without a rule being rewritten.
The same flag is on every field of /client/api/vocabulary, where a
machine should read it.
A second example: a mention spike, once the attention source is connected
This is the request people actually ask for — "tell me when it gets 100 mentions on X" — and it is the reason the attention fields are in the vocabulary. It needs the attention source, which is not connected on this deployment yet, so read the Available column above before you copy this one.
{
"dsl_version": "watcher.v1",
"name": "Mention spike on a listed coin",
"universe": {"chain": ["base", "solana"], "trending": true},
"when": {"all": [
{"above": {"left": "attention_mentions", "right": 100}},
{"above": {"left": "liquidity_usd", "right": {"value": 50, "unit": "k"}}}
]}
}
Note the two shapes. The first example wraps the document under rule
so it can also set delivery settings; this one is the bare
document. /rules
and /rules/validate
accept either,
because a document is what the vocabulary's own skeleton looks like and the
obvious first attempt should not fail on a technicality.
Signal alerts
The other rule class. A signal alert declares signal.v1
and is four required values — the setup, the venue, the market and the
timeframe — plus optional params
and an optional note, capped at 280 characters. For
this class those four values are
the rule, which is why they sit in
the document rather than beside it. Delivery settings — channel_ids, cooldown_seconds,
one_shot
— stay outside it, exactly as they do for a watcher.
POST /client/api/signal-alerts
Authorization: Bearer dmt_live_…
Content-Type: application/json
{
"dsl_version": "signal.v1",
"setup": "rsi_oversold",
"exchange": "coinbase",
"symbol": "BTC-USD",
"timeframe": "h1",
"note": "the hourly dip screen"
}
There is no name
to set, and no name column behind one. The name
in every response is generated from the rule itself — "RSI crosses into oversold on the 1 day — SOL-USD (Coinbase)" — so a label can never
disagree with the thing it labels. note
is your own text.
A symbol is only meaningful next to its exchange: the same pair is SOL-USD
on one venue and SOLUSDT
on another. Matching
ignores case, and what gets stored is the venue's own spelling — so diff the
canonical document that comes back from /client/api/signal-alerts/validate
against what you sent. Venues: Coinbase, Binance.US, Bybit.
The setups
Curated and closed. Each one maps to a single classical condition that can be explained in a sentence, and a structure that is not in this table has no representation to reach. Buy watch and Sell watch name the side a structure signals, because a structure signals one side and offering the other would teach a false reading. Neither is an instruction to trade.
Timeframes are per setup and the narrower constraint: a golden cross is defined on daily and weekly closes, and on an hourly candle the same arithmetic is noise rather than the thing traders mean by the term. Warm-up is closed bars on the alert's own timeframe. A setup will not evaluate until the market has that many; on a weekly timeframe that is years of history.
| Setup | Side | Timeframes | Warm-up | What it means |
|---|---|---|---|---|
|
ltf_hf_buy
LTF breakout with volume (buy watch)
|
Buy watch | m15 | 20 bars |
Version 1 fires when a confirmed, closed 15-minute candle closes strictly above the highest high of the preceding 20 consecutive candles, and its base-asset volume is at least 1.5 times their average volume. The current candle is excluded from both baselines; missing bars or unavailable volume block evaluation. After firing, it re-arms only when a later close returns inside or onto that side of the rolling 20-bar range. Cooldown and one-shot settings still apply. This is a documented range-break approximation, not a reproduction of a TradingView HF indicator, an intrabar high-frequency strategy, or a recommendation to trade. |
|
ltf_hf_sell
LTF breakdown with volume (sell watch)
|
Sell watch | m15 | 20 bars |
Version 1 fires when a confirmed, closed 15-minute candle closes strictly below the lowest low of the preceding 20 consecutive candles, and its base-asset volume is at least 1.5 times their average volume. The current candle is excluded from both baselines; missing bars or unavailable volume block evaluation. After firing, it re-arms only when a later close returns inside or onto that side of the rolling 20-bar range. Cooldown and one-shot settings still apply. This is a documented range-break approximation, not a reproduction of a TradingView HF indicator, an intrabar high-frequency strategy, or a recommendation to trade. |
|
rsi_oversold
RSI crosses into oversold
|
Buy watch | m15, h1, h4, h12, d1, w1 | 50 bars |
RSI (Relative Strength Index) runs 0–100 and measures how one-sided recent moves have been. Below 30 is traditionally called "oversold" — selling has dominated. Traders watch it as a place where a bounce *may* form, but a market can stay oversold for a long time in a strong downtrend. |
|
rsi_recovery
RSI recovers out of oversold
|
Buy watch | m15, h1, h4, h12, d1, w1 | 50 bars |
This fires when RSI climbs back *above* 30 after being oversold — the momentum downturn easing rather than the dip itself. Many traders consider the turn back up a cleaner reference point than the moment price first became oversold. |
|
rsi_overbought
RSI crosses into overbought
|
Sell watch | m15, h1, h4, h12, d1, w1 | 50 bars |
Above 70, RSI is traditionally "overbought" — buying has dominated recently. It's watched as a spot where upward momentum *may* be stretched, but like oversold it can persist through a strong uptrend, so it is a caution flag, not a top. |
|
golden_cross
Golden cross (50 over 200)
|
Buy watch | d1, w1 | 260 bars |
A "golden cross" is the 50-period moving average crossing above the 200-period one — a slow, trend-following signal that the medium-term average has overtaken the long-term average. It is defined on daily and weekly closes; on intraday candles the same math is dominated by noise and isn't what traders mean by the term. |
|
death_cross
Death cross (50 under 200)
|
Sell watch | d1, w1 | 260 bars |
The mirror of the golden cross: the 50-period average crossing *below* the 200-period one, read as the trend turning down over a medium horizon. Also a daily/weekly signal — it lags by design, describing a shift that has already begun. |
|
price_reclaims_ma
Price reclaims a moving average
|
Buy watch | h4, h12, d1, w1 | 80 bars (period 50); 230 bars (period 200) |
Fires when price closes back above its moving average after being below it. The moving average is a common reference for the prevailing trend, so reclaiming it is watched as short-term strength. Choose the 50 for a medium reference or the 200 for the long-term one.
Params: period — one of 50, 200, default 50
|
|
price_loses_ma
Price loses a moving average
|
Sell watch | h4, h12, d1, w1 | 80 bars (period 50); 230 bars (period 200) |
Fires when price closes below its moving average after being above it — the mirror of reclaiming it, watched as short-term weakness relative to the trend reference. The 50 tracks a medium horizon, the 200 the long-term one.
Params: period — one of 50, 200, default 50
|
|
bollinger_lower
Price pierces the lower Bollinger band
|
Buy watch | h1, h4, h12, d1 | 40 bars |
Bollinger Bands sit two standard deviations either side of a 20-period average, so they widen and narrow with volatility. A close below the lower band means price is unusually far below its recent average — a stretch that sometimes mean-reverts, though in a strong down-move price can "walk the band" lower. |
|
bollinger_upper
Price pierces the upper Bollinger band
|
Sell watch | h1, h4, h12, d1 | 40 bars |
A close above the upper Bollinger band means price is unusually far above its recent 20-period average — an extension that can mean-revert, but which also occurs during strong breakouts, so it reads as "stretched," not "reversing." |
|
macd_bullish
MACD bullish signal cross
|
Buy watch | h4, h12, d1, w1 | 80 bars |
MACD tracks the gap between a fast and a slow moving average, and a "signal line" smooths that gap. The MACD line crossing above the signal line is a classic momentum-turning-up marker. It whipsaws on short timeframes, so it's offered on 4h and above. |
|
macd_bearish
MACD bearish signal cross
|
Sell watch | h4, h12, d1, w1 | 80 bars |
The MACD line crossing *below* its signal line — the mirror of the bullish cross, read as momentum turning down. Also offered on 4h and above, where the signal is less prone to whipsaw. |
|
msb_bullish
Bullish structure break (new higher high)
|
Buy watch | m15, h1, h4, h12, d1, w1 | 60 bars |
Markets that are falling make a series of lower highs. When price finally closes above the most recent one, that sequence is broken — traders call it a bullish market-structure break (MSB, or BOS). It is the moment a downtrend stops making lower highs, which is why it sits under Buy watch: it is the condition someone looking for a long waits for. The level itself is a "swing high" — a bar standing above the two bars on each side of it. Needing bars on BOTH sides is what makes it a pivot rather than just a recent high, and it is also why it can only be confirmed two bars after it printed. This alert fires on a break of a confirmed level, never a guessed one. It marks the end of the old structure, not the start of a reliable new one. Plenty of structure breaks fail inside an ongoing downtrend, and a condition being met is information, not advice. |
|
msb_bearish
Bearish structure break (new lower low)
|
Sell watch | m15, h1, h4, h12, d1, w1 | 60 bars |
Markets that are rising make a series of higher lows. When price closes below the most recent one, that sequence is broken — the mirror of the bullish case, and the moment an uptrend stops making higher lows. It sits under Sell watch because it is the condition someone looking to exit a long, or to short, watches for. The level is a "swing low": a bar sitting below the two bars on each side of it. Because it needs bars on both sides to be a pivot, it is confirmed two bars after it printed, and this fires only on a break of a confirmed level. As with the bullish case, it marks the end of the previous structure rather than the start of a dependable new one. A condition being met is information, not advice. |
Only the two moving-average setups take a param, and it is the one choice the
catalog leaves open: the 50 for a medium trend reference or the 200 for the
long-term one. Asking for anything else is an error rather than a silent
correction, so you learn what the setup offers instead of being quietly given
the default. This example also shows the wrapped request shape, which is how
you set delivery settings — both shapes are accepted here and on /client/api/rules.
{
"rule": {
"dsl_version": "signal.v1",
"setup": "price_reclaims_ma",
"exchange": "coinbase",
"symbol": "SOL-USD",
"timeframe": "d1",
"params": {"period": 200}
},
"cooldown_seconds": 86400,
"one_shot": false
}
Whether a market can carry a setup is a separate question
The table above is the whole static contract, and it is deliberately silent
about availability. Whether golden_cross can run on a given market
today depends on how much history and how much traded volume that market has —
facts about one market at one moment, which a table published here would be
wrong about within the day.
Ask /client/api/signal-alerts/availability?exchange=…&symbol=…
instead. It answers per setup and per timeframe, and where the answer is no it
says why in a code you can branch on, with the numbers behind it and the
sentence a person would be shown:
GET /client/api/signal-alerts/availability?exchange=coinbase&symbol=SOL-USD&setup=golden_cross
{
"data": {
"exchange": "coinbase",
"exchange_label": "Coinbase",
"symbol": "SOL-USD",
"setups": [
{
"id": "golden_cross",
"label": "Golden cross (50 over 200)",
"side": "buy_watch",
"timeframes": [
{"id": "d1", "label": "1 day", "available": true,
"reason": null, "short": null, "detail": null},
{"id": "w1", "label": "1 week", "available": false,
"reason": "insufficient_history",
"bars_held": 214, "bars_needed": 260,
"short": "Not enough history",
"detail": "This market has 214 weekly closes; the setup needs 260."}
]
}
]
}
}
This matters most on the long timeframes. A weekly moving-average cross warms up
on 260 weekly closes — five years of history — and without asking first, the
only way to discover that is to arm the alert and wait. That endpoint is the one
call here that loads a market's candle history, so it is metered per key; over
the limit is 429 with a retry-after header, which is
a throttle and not an authorization failure.
Error codes for this class: empty, invalid_type, invalid_value, missing_key, not_an_object, out_of_range, too_long, too_many_keys, unknown_key, unknown_market, unsupported_timeframe, unknown_plan_ref, invalid_plan_target, invalid_plan_offset, plan_too_many_targets, wrong_dsl_version. Two of them
are its own — unknown_market
when the venue does not list the
symbol, and unsupported_timeframe
when the setup is real but is not defined on the timeframe you asked for.
When a rule is rejected
The validator returns every problem in the document at once, not the first one, each with a path into the document and usually a concrete correction. Correcting one error per round trip is slow, and worse, a fix for an early error can quietly create a later one.
HTTP/1.1 422 Unprocessable Content
{
"error": {
"code": "invalid_rule",
"dsl_version": "watcher.v1",
"message": "The rule was not accepted, and nothing was saved. 2 problems were found; every one of them is listed in \"errors\". Fix them all and resubmit.",
"errors": [
{
"path": "when.all[0].above.left",
"code": "unknown_field",
"message": "\"liqidity_usd\" is not a field this engine computes, so the condition was not applied.",
"expected": ["liquidity_usd", "pool_age_hours", "volume_24h_usd", "…"],
"got": "liqidity_usd",
"suggestion": "did you mean \"liquidity_usd\"?"
},
{
"path": "when.all[1].cross_above",
"code": "unsupported_operator",
"message": "\"cross_above\" is not valid on \"liquidity_usd\".",
"expected": ["above", "below", "between"],
"got": "cross_above",
"suggestion": "\"liquidity_usd\" has no previous-bar value, so a cross could never be true — use \"above\" with once_per_asset to fire on the first crossing"
}
]
}
}
Codes are a closed set: ambiguous_node, empty, invalid_range, invalid_type, invalid_unit, invalid_value, missing_key, not_an_object, out_of_range, too_deep, too_expensive, too_long, too_many_conditions, too_many_keys, too_many_nodes, unknown_field, unknown_key, unknown_operator, unsupported_operator, wrong_dsl_version, wrong_scope.
MCP
The same operations are available over Model Context Protocol at /client/api/mcp
— JSON-RPC 2.0 over HTTP POST, with the key as a bearer token. Both rule classes are there, and a tool's name says which one it
speaks for.
Coin watchers: get_vocabulary, validate_rule, create_rule, list_rules, get_rule, pause_rule, resume_rule, delete_rule, get_fires.
Signal alerts: get_signal_setups, check_signal_availability, validate_signal_alert, create_signal_alert, list_signal_alerts, get_signal_alert, update_signal_alert, pause_signal_alert, resume_signal_alert, delete_signal_alert, get_signal_alert_fires.
And get_usage, for the account both classes are counted against.
Scopes apply per tool, not per connection: a rules:read
key sees
the read tools of both classes and is refused every write. tools/list
returns only what your key can call. The two catalog tools — get_vocabulary
and get_signal_setups
— need no scope beyond a valid key, because
they publish what the engine speaks and say nothing about any account.
check_signal_availability
needs market:read
and spends the same per-key allowance as GET /client/api/signal-alerts/availability. It is the same candle load
either way, so it is one budget rather than one per protocol.
A prompt you can hand your own AI
Paste this, with your key in place of the placeholder.
You can use the DMTrading rules API at https://app.dutchmantrading.com/client/api.
Use my API key as `Authorization: Bearer <key>`; never repeat it in replies.
Start with GET /client/api/capabilities (get_capabilities over MCP).
Resolve symbols with /markets; use /ohlcv for chart analysis, without a rule.
Preflight rules at /preflight; a valid document may still have readiness blockers.
For multiple exchange markets use basket.v1 at /signal-baskets, with
/signal-baskets/preflight first. Fixed symbols or top-N volume membership;
one signal slot per active/paused member. Read back status after creation.
(If you speak MCP, connect to https://app.dutchmantrading.com/client/api/mcp
with that key as a bearer token instead, and use the tools it offers. Both
kinds of rule below are there: the *_rule tools are coin watchers and the
*_signal_alert tools are signal alerts, and every step below has a tool with
the obvious name — get_vocabulary, get_signal_setups,
check_signal_availability, validate_rule, validate_signal_alert.)
There are two kinds of rule. Decide which one I am asking for before you write
anything:
* A COIN WATCHER, at /client/api/rules, when I have not named a coin — "any
Solana coin that does X". It finds the coins itself.
* A SIGNAL ALERT, at /client/api/signal-alerts, when I name a market on an
exchange and a classical technical setup — "tell me when SOL-USD goes
oversold on the daily". The setups are a fixed list, not a formula.
If I asked for a named market, do not build a watcher for it, and if I asked
for a screen across coins, do not look for a setup. Say which one you picked.
For a coin watcher, GET /client/api/vocabulary first and read the field table.
The vocabulary is closed and versioned: a field that is not in it is rejected,
not ignored, and a document declaring the wrong dsl_version is rejected whole.
Note which fields accept cross_above / cross_below — a cross on a field with no
previous-bar value is refused, because it could never be true.
Read each field's "available" flag in that response, and prefer fields where it
is true. A field with available false is still accepted by the validator, so a
rule using one looks correct and then holds on every pass instead of firing;
the response lists them under "unavailable_fields" with the reason. If what I
asked for needs one of those fields, tell me that before writing the rule,
rather than after it has been quiet for a week.
For a signal alert, GET /client/api/signal-alerts/schema first and read the
"learn" text on each setup, not just the ids — that text is how you tell which
setup answers what I actually asked. Each setup lists the timeframes it is
defined on, and those are narrower than the global list.
Then GET /client/api/signal-alerts/availability?exchange=…&symbol=… before you
create it, and tell me if what I asked for is not available on that market yet.
A weekly moving-average cross needs 260 weekly closes — five years — and if you
skip this step, the first thing either of us learns is silence.
When I describe an alert I want:
1. Write it as a document of the class you picked.
2. POST it to that class's /validate endpoint and correct it against what
comes back. The response lists every problem at once, each with a path
into the document and usually a concrete fix. Iterate here — it stores
nothing.
3. Once it validates, show me the plain-English `summary` from the response
and ask me to confirm that is what I meant. The summary is generated from
the canonical rule, so it says what the engine will do, not what I said.
4. Only then POST it to the create endpoint for that class.
When I ask how a rule is doing, GET its /fires endpoint and read it to me
straight: the number of scored fires, how many are still pending, and how many
are unavailable because there was no price at the horizon. Do not treat
unavailable as a flat 0% and do not quote a hit rate without saying how many
fires it is computed over.
Do not tell me what to buy or sell, and do not predict prices. This API reports
that a measurement crossed a line and reports its own record of what happened
afterwards. That record is the only claim about how well a rule has worked.