Auto trade bot guide

Build a Bot

Receive Sentinel signals, decide entries, size risk and place orders on your own exchange account.

https://ribqa.com · public, no account needed to read signals

Ways in
3REST · SSE · MCP
Endpoints named
16from the API catalog
Cost model
0.12%per trade, fees and funding
Settling guard
96hbefore a trade enters a rate

1. Get signals

REST · SSE stream · MCP

RouteNeedsUse it for
REST, publicNothingPolling the pending list and the ledger on a schedule.
SSE streamSession token or a stream ticketSignals, vetoes and invalidations the moment they are published.
MCP serverAPI key from Account > API keysAI agents and scripts that speak JSON-RPC. Read-only: no orders.
MethodPathAccessWhat it is
GET/api/public/signals/pendingpublicPublished signals that have not matured yet.
GET/api/public/signalspublicPublished signals visible without an account.
GET/api/public/signals/{id}publicOne published signal: levels, engines, combo, plan, veto context, outcome net of the cost model, and headlines naming its coin within 24 h.
GET/api/public/ledger/recentpublicNewest published signals with their outcome and net result, up to 500.
Poll pending signals
# Published signals with no outcome yet. No account, no header.
curl "https://ribqa.com/api/public/signals/pending?limit=50"

# Response: {"signals":[...],"data":[...],"total":N}. signals and data are the same list.
# One signal with its plan, veto context and outcome:
curl "https://ribqa.com/api/public/signals/<id>"
MethodPathAccessWhat it is
POST/api/v1/auth/loginpublicPassword sign-in; returns a token or a 2FA challenge.
POST/api/v1/auth/login/2fapublicSecond step of a 2FA sign-in, with a TOTP or recovery code.
POST/api/v1/auth/refreshpublicExchange a refresh token for a new access token.
POST/api/v1/stream/ticketsessionIssue a short-lived single-use ticket for the SSE stream.
GET/api/v1/streampublicneeds bearer token or stream ticketSSE event stream; it carries no auth middleware, but the handler itself requires a bearer header or a stream ticket.
Sign in
# Sign in with your own credentials. This page stores none.
curl -X POST "https://ribqa.com/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"<your-email>","password":"<your-password>"}'

# Response: access_token, refresh_token, expires_in (seconds), user.
# With two-factor on, the response is two_factor_required, challenge and
# expires_in instead, and the tokens come from /api/v1/auth/login/2fa.
export TOKEN="<access_token>"
Refresh
# access_token lifetime: 7200s default, 43200s with remember_me,
# 86400s with keep_signed_in. Refresh before it expires.
curl -X POST "https://ribqa.com/api/v1/auth/refresh" \
  -H "Content-Type: application/json" \
  -d '{"refresh_token":"<refresh_token>"}'

# The refresh token is rotated. Store the new one from the response.
Subscribe to the stream
# Option A: bearer header.
curl -N "https://ribqa.com/api/v1/stream" -H "Authorization: Bearer $TOKEN"

# Option B: a single-use ticket, for clients that cannot set headers.
curl -X POST "https://ribqa.com/api/v1/stream/ticket" -H "Authorization: Bearer $TOKEN"
# Response: {"ticket":"...","expires_in_seconds":45,"stream_path":"/api/v1/stream"}
curl -N "https://ribqa.com/api/v1/stream?ticket=<ticket>"

# A ticket is valid once, for 45 seconds. Request one per connection.
# ?token=<access_token> is refused in production.
EventPayload
first frame, unnamed{"event":"connected","client":"..."}
signalA full signal object, the same shape as the REST row.
invalidation{"signal_id","symbol","reason"}
vetosignal_id, symbol, direction, reason_code, reason_text, trigger, BTC context, vetoed_at
btc_chart_readid, read_at, price, direction, previous_direction, confidence, reason
heartbeat{"ts"}, every 15 seconds. A gap means the connection is gone.
MethodPathAccessWhat it is
POST/api/v1/mcppublicneeds MCP API key for tools/callMCP remote (Streamable HTTP, JSON-RPC 2.0). initialize, ping and tools/list are open; tools/call needs an MCP API key.
MCP: open signals
# API key: Account > API keys. Shown once; keep it out of URLs.
curl -s https://ribqa.com/api/v1/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SENTINEL_MCP_KEY" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"get_open_signals","arguments":{"limit":20}}}'

Every value in angle brackets or after a $ is a placeholder. Put your own credentials in your own shell, never in a query string: the stream ticket exists so a URL never has to carry a token. The full tool list is on the MCP page.

2. Read a signal

20 fields

FieldTypeWhat it means
idstringSignal id. The same value on the stream frame and the REST row.
symbolstringBinance pair name, for example SOLUSDT.
market_typespot | futuresWhich Binance market the candles came from. futures is a USDT-M perpetual with no spot market. Absent on older rows means spot.
timeframestringCandle interval the setup was detected on. Published signals are 4h.
directionlong | shortSide of the setup.
modesmc_only | ind_only | hybridWhich engine family produced it: structure, indicators, or both.
entrynumberEntry price. A signal whose entry is never touched is not a trade.
slnumberStop loss, placed at 2.5x ATR.
tpnumber[]Three targets, nearest first: tp[0] is TP1, tp[1] TP2, tp[2] TP3. The ledger books a win at TP1.
rrnumberPlanned reward:risk from entry, stop and TP1. An intention recorded before entry.
confidencenumber, 0 to 1Evidence score: how much of the engine stack agreed. Measured AUC 0.496, so it is not a win probability.
confluence{ source, score, weighted }[]Per-engine contribution behind that score.
combostringMost specific matched combo id, empty for an unmatched signal.
regimeBull | Bear | Sideways | TransitionMarket regime recorded at scan time. The only regime value known before entry.
investment_scorenumberInternal ranking score. Not a return estimate.
management_plan{ breakeven_at_r, partial_at_r, partial_fraction }Stop to breakeven at breakeven_at_r R; partial_fraction of the position off at partial_at_r R. Absent when the signal is unmanaged.
regeneratedbooleanThis signal replaced an earlier one for the same setup.
regenerated_atRFC3339When that replacement happened.
created_atRFC3339Publication time. The 96-hour settling guard keys off this, not off evaluation time.
expires_atRFC3339End of the signal's window. A position still open at this point ages out unresolved.

A signal is a setup, not a fill. Your bot decides whether the entry was touched, and the ledger on this site counts a position only once it was. The entry fill rate is published on the signals ledger, with its denominator.

confidence is an evidence score, not a probability. Its measured AUC is 0.496, so ranking or sizing by it does not separate winners from losers.

3. Decide entries

filters · drift · BTC veto

CheckWhy
New since you startedThe pending list holds every published signal without an outcome, including old ones. Act on ids you have not seen, or check created_at and expires_at.
Entry driftCompare the live price with entry before you enter. Aleph Edge refuses an entry when price has already crossed TP1 or the stop, or moved more than 0.5% against entry.
market_typefutures means a USDT-M perpetual with no spot market. A spot-only bot must skip it.
Direction and symbolYour own lists. A spot account cannot short; skip short signals there.
Engine and comboconfluence[0].source is the primary engine and combo the matched combo id. The live table below is keyed the same way.
BTC veto contextA veto event on the stream, or a row in /api/public/signals/vetoed, cancels a signal. btcd_veto_long and pulse_alert on /api/v1/market/btc-macro are what Aleph Edge reads to pause new longs.
MethodPathAccessWhat it is
GET/api/public/signals/vetoedpublicSignals the guard cancelled, with the reason; registered only when the repository can list them.
GET/api/v1/market/btc-macropublicBTC macro context: trend state, volatility and dominance.
GET/api/public/statspublicPublic summary counters from the ML outcome store, cached for 60 seconds.
TermWhat happenedWhere it lands
VetoSentinel cancelled a published signal because BTC's regime turned, or BTC moved more than 2% in an hour against it.Its own feed, and its own line on every money panel.
Vetoed, never enteredThe entry was never touched before the cancel.Not a trade. It is a decision of ours, and counting it would let us grade our own vetoes.
Vetoed, already enteredThe position existed and was closed early by the guard.veto_closed_count, veto_closed_total_pnl_percent, and the net twins beside them.

A veto close is excluded from the win rate because it has no verdict: the thesis was never allowed to reach its target or its stop. It is also excluded from the headline money scope, and published on its own line, so nothing is hidden and the two reconcile: entered_positions plus veto_closed_count equals all_entered_positions.

Engines, live, matured cohort

Loading the live engine table.

4. Risk

sizing · stop · target · plan

Size from the stop, not from a fixed amount
stop_distance = abs(entry - sl) / entry
notional      = capital * risk_pct / 100 / (stop_distance + 0.0012)
leverage      = min(notional / capital, your_leverage_cap)
DecisionWhat the signal gives youNote
Sizeentry and sl.With a 1% risk and a 3% stop, a stop-out costs about 1% of capital including the 0.12% cost. Aleph Edge defaults to 1% and accepts 0.1% to 5%.
Stopsl, placed at 2.5x ATR.Place it on the exchange when you open the position. A stop that lives only in your script is gone when the script is.
Targettp[0] to tp[2]: TP1, TP2, TP3.The published win rate is measured at TP1. A farther target is reached less often; measure its rate on your own paper run.
Breakevenmanagement_plan.breakeven_at_r.Move the stop to entry once price has gone that many R in your favor. A later hit there is a scratch, not a win.
Partialmanagement_plan.partial_at_r and partial_fraction.Take that fraction off at that many R, but only when that level is nearer than your target.
Timeexpires_at.The ledger ages a position out at expiry. Aleph Edge closes at the market price 72 hours after it.

5. Execute on an exchange

your keys, your account

RuleDetail
Your own exchange APISentinel never holds exchange keys and never places exchange orders. No route of the API or the MCP server trades.
Trade-only keyNo withdraw permission. Restrict it to your IP where the exchange allows it.
Symbol mappingsymbol is a Binance name. Other exchanges spell pairs differently, for example BTC-USDT-SWAP on OKX.
Protective ordersStop and take profit as exchange-side orders, placed right after the fill.
IdempotencyA client order id per signal id, so a retry after a timeout cannot open a second position.
One position per signalThe same signal can arrive more than once, for example after a reconnect. Keep the ids you have acted on.
Minimal loop (Python, prints instead of ordering)
import time, requests

API = "https://ribqa.com"
RISK_PCT, CAPITAL, COST = 1.0, 1000.0, 0.0012  # 0.12% round trip + funding
MAX_DRIFT = 0.005                               # skip if price moved 0.5% against entry

def pending():
    r = requests.get(f"{API}/api/public/signals/pending", params={"limit": 100}, timeout=10)
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", "60")))
        return []
    r.raise_for_status()
    return r.json()["signals"]

def price(sig):
    host = "fapi" if sig.get("market_type") == "futures" else "api"
    path = "/fapi/v1/ticker/price" if host == "fapi" else "/api/v3/ticker/price"
    r = requests.get(f"https://{host}.binance.com{path}", params={"symbol": sig["symbol"]}, timeout=10)
    return float(r.json()["price"])

seen = {s["id"] for s in pending()}  # act only on signals published after start

while True:
    time.sleep(200)  # anonymous limit is 20 requests an hour
    for sig in pending():
        if sig["id"] in seen:
            continue
        seen.add(sig["id"])
        long = sig["direction"] == "long"
        entry, sl, tp1, now = sig["entry"], sig["sl"], sig["tp"][0], price(sig)
        drift = (now - entry) / entry * (1 if long else -1)
        crossed = (now >= tp1 or now <= sl) if long else (now <= tp1 or now >= sl)
        if drift > MAX_DRIFT or crossed:
            print("skip", sig["symbol"], f"drift {drift:+.2%}")
            continue
        stop = abs(entry - sl) / entry
        notional = CAPITAL * RISK_PCT / 100 / (stop + COST)
        print(sig["symbol"], sig["direction"], f"notional {notional:.0f} USDT", "sl", sl, "tp1", tp1)
        # place_order(...)  your exchange client, on a paper or testnet account first

Prefer not to write the execution side yourself? Aleph Edge is our desktop bot for these signals. It runs in paper mode today.

6. Paper-test first

testnet or paper account

The published ledger is gross of execution effects

It is settled on Binance candles at the published levels. It has no slippage, no missed or partial fills, no latency and no exchange outage in it. Its net figures charge the 0.12% cost model and nothing else. Your fills will differ from it.

Run the bot on an exchange testnet or a paper account before any real balance, long enough to see a run of stop-outs. Then compare your results with /api/public/ledger/recent signal by signal: the gap between the two is your execution cost.

7. Costs and settling

0.12% per trade · 96-hour guard

ComponentValueBasis
Taker fee, per side0.05%Binance USDT-M futures, VIP0, no BNB discount.
Round trip fee0.10%Two sides of one position.
Funding, per trade0.02%About two 8-hour intervals over a median hold. A long pays it, a short receives it.
Modelled cost per trade0.12%Round trip plus funding. Every net figure published here is charged this.
net_total = gross_total - (0.10 * n) - (0.02 * (n_long - n_short))

Funding is not symmetric. Fees are paid by whoever trades, both directions; funding is a transfer, and while the rate is positive a long pays it and a short receives it. Charging a flat 0.12% to every trade understates the short side, which is the side this project is judged on.

Every pnl_percent the API publishes is gross, price distance only. The cost model is applied on top, once, and the net fields are named net_*. veto_closed_total_pnl_percent is gross, like every pnl_percent; its net twin is veto_closed_net_total_pnl_percent. The live cost_model object is on the ML stats payload, so a client can read the current assumption rather than copy these four numbers.

OutcomeDefinitionIn the win rate?
successTake profit reached.Yes, once matured.
failStop loss reached.Yes, once matured.
settlingDecisive, but the signal was published less than 96 hours ago.No, not yet. Published as maturing_count, maturing_wins, maturing_losses.
scratchStop ratcheted to entry under the management plan and hit there, with no offsetting partial.No. Neither a win nor a loss. Published as scratch_count with its own average.
invalidatedClosed early by the BTC guard.No. See vetoes above.
expired_unresolvedAged out of its window without touching either barrier.No, but it stays in the money.

Why 96 hours

Winners settle faster than losers. Measured on this book, a decisive win closed in a median 14.1 hours against 20.8 for a loss, so a ratio taken over everything closed so far counts landed wins against losses still in flight. Unguarded that read 77.4%; the cohort past 96 hours read 50.0%. The guard is the difference between those two numbers.

A breakeven scratch is outside the rate because it has no verdict, not because it is convenient. It is published on its own so the rate can never be raised by quietly scratching losers, and its measured average is published with it: assuming a scratch costs nothing is an assumption, and this one was tested.

8. Limits

per minute · per hour

TierPer minutePer hourBucket
Anonymous3020By client IP
Free120100By user id
Trial60500By user id
Gold3002000By user id
Platinum9008000By user id
Aleph150020000By user id

Both limiters run on every request and whichever is exceeded first returns the 429. Anonymous callers get 20 requests an hour, so a public-data bot should poll on a schedule, not in a loop.

On a 429Do this
Retry-AfterSleep for exactly that many seconds. It is on every 429 and it is authoritative.
Response bodyJSON: {"error":"rate_limit_exceeded","tier":"...","retry_after":N}, with "window":"minute" when the minute bucket was the one that tripped. Parse it as JSON; the Content-Type says text/plain.
X-RateLimit-RemainingPresent on every response, alongside X-RateLimit-Limit, X-RateLimit-Reset and X-RateLimit-Tier. Back off before you reach zero rather than after.
X-RateLimit-Minute-RemainingThe minute bucket's own counter, with its own Limit and Reset headers. Watch both.
Do notRetry immediately, and do not spread the same key across parallel workers. The bucket is per user, not per connection.

Honesty note

MethodPathAccessWhat it is
GET/api/public/statspublicPublic summary counters from the ML outcome store, cached for 60 seconds.
GET/api/public/ml/statspublicAggregated ML outcome statistics; registered only when the ML service is wired.
GET/api/v1/ml/statssessionThe same aggregate as the public route, behind a session.

Every ratio on this site carries its denominator. A win rate with no n is not a measurement, and a percentage taken over one population printed beside a percentage taken over another is the defect this page exists to avoid: matured counts and raw observation counts are different numbers and are never mixed.

Below 20 settled trades no rate is published at all, on this page or anywhere else on the site. Our outcomes also cluster by day, so a raw sample size overstates the evidence behind it; effective_sample_size on the ML stats payload is the honest count.

This is data, not financial advice

Sentinel Aleph publishes market intelligence and signal tooling. It does not manage funds, it makes no recommendation to buy or sell anything, and it guarantees no outcome. What you build against this API, and what you risk on it, is yours.
Offline