GHOST // PAPER BOT APIFRIENDS' BOTS · SAME LAB · FAKE MONEY
Anyone can create an account on the website or in the iPhone app. No invite code. Mint a key in Settings on your dashboard. It is shown once, stored as a hash, and can trade only that account. Send it as Authorization: Bearer gst_…. A browser session cookie does nothing here.
RULES THE BUG WILL ENFORCE
- The key places paper trades on that account. The shared crowd signal stays on the desk and is not copied into anyone's wallet.
- Stake is paper dollars, at least $0.01, and no more than the free balance (open stakes are reserved).
- Side is
LONGorSHORT. The position opens at the current BTCUSDT price and stays open until you close it. P&L is(exit − entry) / entry × stake × leverage. Shorts invert that. Leverage is 1, 2, 5, 10, 25, 50, or 100. If you leave it out, it is 1. A loss cannot take the paper balance below zero. margin_modeiscross(the default, same idea as Binance) orisolated. Isolated: the most that position can lose is its stake, and the liquidation price is computed from that stake. Cross: the available paper balance backs that position and the other cross positions. When their combined losses reach that balance, the cross positions close. The liquidation price is computed from the available balance. Isolated margin stays put while cross positions close.- Do not send
horizon. If you sendsymbolit must be BTCUSDT. A position that closes because price crossed its liquidation price is still aLOSS, withliquidatedtrue. - Older settled calls stay in history with the result they already have.
- Any field that looks like an exchange secret (
api_key,secret,binance,mnemonic, and the rest of that family) is rejected. Nothing from that body is stored. - Bot trades show a robot mark on the leaderboard and the high scores. Win records rank by percent of the balance at the close. A grant is not a score.
- A desk refill follows the account plan. Free is one a week, up to $1,000. Basic ($0.99/mo) is one a week, up to $10,000. Expert ($4.99/mo) is unlimited, up to $10,000 each. Lifetime ($69.99 once) is permanent Expert and does not expire. A new account gets a 3-day Expert trial, then Free.
POST /api/v1/reloadignores the plan: $0 to $10,000, once per 24 hours. An admin grant is not a reload. A refill sets the paper cash. History, open positions, grants, and the capital base stay. Plans do not change high scores.
GET /api/v1/signal
Shared crowd signal. Bull percent, last call, and the BTC price. No account data.
curl -s \ -H "Authorization: Bearer gst_paste_the_key_here" \ https://your-dashboard.example/api/v1/signal
GET /api/v1/account
Your paper balance, open trades, and up to 100 history rows. An ADMIN GRANT row is a paper adjustment, not a trade, and its pnl is null. Performance is the wallet versus deposited capital: starting balance plus net grants.
curl -s \ -H "Authorization: Bearer gst_paste_the_key_here" \ https://your-dashboard.example/api/v1/account
POST /api/v1/reload
Paper-key reload. JSON body: {"balance":250}. This ignores the desk plan. The number is $0 to $10,000, and the account can do this once per 24 hours. It is written to the public reload log and tracker and counted on the high score badges. It sets the paper cash. History and open positions stay. They are not closed and they are not hidden. A second reload inside 24 hours is refused and the wallet stays put. Desk refills in Settings follow Free, Basic, and Expert.
POST /api/v1/subscription/apple
Reserved for an App Store receipt. It is not active. A signed-in key gets apple receipt verification is not active yet and the plan does not change. The receipt is not stored. A body may name the non-consumable ghostlab.expert.lifetime. That product is recognized as Lifetime and still does not grant the plan. The iOS app does not call this route. A signed-in desk session posts the StoreKit 2 transaction to POST /api/subscription/apple, and App Store Server Notifications Version 2 post to POST /api/apple/notifications. Neither stores the signed payload. Steps for the three products are in docs/APP_STORE_IAP_SETUP.md.
curl -s -X POST \
-H "Authorization: Bearer gst_paste_the_key_here" \
-H "Content-Type: application/json" \
-d '{"balance":250}' \
https://your-dashboard.example/api/v1/reload
POST /api/v1/trades
Open one paper position. Same as POST /api/v1/positions. JSON body: {"side":"LONG","stake":10,"leverage":5,"margin_mode":"cross","bull_pct":67.9}. Leave leverage and margin out and you get 1× cross. SHORT and isolated work the same way. It does not expire. bull_pct is the crowd bull percent of the sample that opened the trade, a number from 0 to 100. The desk stores that number on the position. Leave it out and the trade keeps the last pushed crowd percent, which is the previous cycle when you open before the push.
curl -s \
-H "Authorization: Bearer gst_paste_the_key_here" \
-H "Content-Type: application/json" \
-d '{"side":"LONG","stake":10,"bull_pct":67.9}' \
https://your-dashboard.example/api/v1/trades
GET /api/v1/positions
Your open positions, with the live mark and unrealized upnl. Unrealized P&L is not in the balance until you close.
curl -s \ -H "Authorization: Bearer gst_paste_the_key_here" \ https://your-dashboard.example/api/v1/positions
POST /api/v1/positions/{id}/close
Close one of your open positions at the current BTCUSDT price. The realized P&L lands in the paper balance. Someone else's id is rejected.
curl -s -X POST \ -H "Authorization: Bearer gst_paste_the_key_here" \ https://your-dashboard.example/api/v1/positions/1/close
GET /api/v1/trades/{id}
One of your trades, open or already closed. Someone else's id is a 404.
curl -s \ -H "Authorization: Bearer gst_paste_the_key_here" \ https://your-dashboard.example/api/v1/trades/1
PYTHON
Stdlib only. Replace the host and paste the key you minted.
import json, urllib.request
KEY = "gst_paste_the_key_here"
BASE = "https://your-dashboard.example"
def call(method, path, body=None):
data = None if body is None else json.dumps(body).encode()
req = urllib.request.Request(
BASE + path, data=data, method=method,
headers={"Authorization": "Bearer " + KEY, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as res:
return json.load(res)
print(call("GET", "/api/v1/signal"))
print(call("GET", "/api/v1/account"))
trade = call("POST", "/api/v1/positions", {"side": "LONG", "stake": 10, "leverage": 5, "margin_mode": "cross", "bull_pct": 67.9})
print(call("GET", "/api/v1/positions"))
print(call("POST", "/api/v1/positions/" + str(trade["id"]) + "/close"))
print(call("GET", "/api/v1/trades/" + str(trade["id"])))