Drive Before the Fill from your own code
The one metered task, check, is available over HTTP. It takes one planned trade - symbol, side, entry, stop, target, multiplier, the account size, the size you meant to place, your win rate, your limits, the positions already open and a line on why you are taking it - and returns a risk read: a verdict (take, resize, wait or skip) that keeps to the page's gate, the size to place, a read of the stop (sound, tight, wide or unclear), what the target and the edge come to, how the book and this trade interact, the events the thesis names, a plan for after the fill, a pre-click checklist and what proves the idea wrong.
The division of labour first: the page counts; the model judges. The page sizes the trade from the stop at your risk per trade, caps it by your position, heat and group limits, works out reward to risk, the break-even win rate, the expectancy at your win rate, the open heat before and after, the losing-streak table and the 1R levels, decides the gate, and sends it all as facts and gate. The model must not recompute them differently. After the reply, the page holds it to the gate and the limits and lists anything that disagrees. Risk arithmetic, not investment advice: the read never says where a price is going.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"before-the-fill"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, symbol, side, currency, account, entry, stop, target, multiplier, planned_size, group, atr, win_rate, avg_win_r, limits, open_book, thesis, gate, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the risk read is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: check (the one task is check); a missing or different one is still answered as check. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always check. |
symbol | yes | The symbol, as you trade it (up to 16 characters). |
side | yes | long or short. |
currency | yes | USD, EUR, GBP, CAD or AUD - the account currency. |
account | yes | The account size. |
entry | yes | The entry price. |
stop | yes | The stop price: below the entry for a long, above it for a short. |
target | yes | The target price, or none. |
multiplier | yes | Money per point per unit: 1 for shares, the contract multiplier for futures. |
planned_size | yes | The size you meant to place, or none. |
group | yes | Your tag for what the trade moves with, or none. |
atr | yes | The average true range you use, or none. |
win_rate | yes | Your win rate in percent, or none. |
avg_win_r | yes | Your average winner in R, or none. |
limits | yes | Your limits as one line: risk per trade, open heat, group and position percents and the size step. |
open_book | yes | The open positions, one a line (SYMBOL side qty entry stop, optional now, x multiplier, #group), or none. |
thesis | yes | Why you are taking it and what proves it wrong, or none (up to 1,500 characters). |
gate | yes | The page's gate: take, resize, wait or skip. |
facts | yes | The page's sums, one line each: the trade, the risk per unit, the limits, the size by each limit and the maximum, the half size, the risk and reward at the maximum, the planned size, reward to risk, your record and this trade's expectancy, the open book line by line, the group, the losing streaks, the 1R levels and the gate with its reasons. Build it with TradeKit.buildInput. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The sums: risk per unit is the entry-to-stop distance plus slippage, times the multiplier. Size by risk is the account times the risk per trade, less the round-trip commission, over the risk per unit, rounded down to the size step. The position limit caps the position's value; the heat limit caps the open risk of the whole book (each held position's risk runs from its current price, or its entry, to its stop); the group limit caps the open risk of one tag. The smallest size wins. Reward to risk is the distance to the target, less slippage, over the risk distance; the break-even win rate and the expectancy assume every trade ends at the target or the stop.
The gate, in order: a negative expectancy at your win rate, or a stop too far for one unit, is skip; no heat or group room is wait; a planned size over the maximum is resize; otherwise take. The reply keeps to it - it may only turn a take into a resize at the half size, and only for a stop under 1 ATR, an event in the holding period the thesis has no plan for, or the thesis saying the book already holds the same idea. The size is the planned size or the maximum for take, the maximum for resize and 0 for wait and skip.
The reply
One JSON object, no prose around it:
{"task": "check", "headline": "...", "verdict": "take", "size": 75, "size_why": "...", "stop": {"read": "sound", "note": "..."}, "target": {"note": "..."}, "edge": "...", "book": "...", "events": ["..."], "plan": [{"when": "...", "do": "..."}], "before_you_click": ["...", "...", "..."], "wrong_if": "..."}
| Field | What it holds |
|---|---|
headline | One sentence: the verdict, the size and the one thing that decided it. |
verdict | take, resize, wait or skip. |
size | A number of units to place (0 for wait and skip). |
size_why | One or two sentences naming the limit that set the size. |
stop | read (sound, tight, wide or unclear) and a note. |
target | A note on the target and reward to risk, or that there is none. |
edge | Reward to risk, the break-even win rate and the expectancy at your win rate, in words. |
book | Open heat before and after, the room left, the group, and anything that looks like the same bet. |
events[] | Up to three forward-looking risks the thesis names; empty if none. |
plan[] | Two to four {when, do} steps for after the fill, or what would have to change for a wait or skip. |
before_you_click[] | Three to five checks to make before placing the order. |
wrong_if | What proves the idea wrong, from the thesis. |
The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a verdict that leaves the gate, a size over the maximum, a non-zero size on a wait or skip, a size off your size step, a stop read that disagrees with the ATR multiple, a plan step that widens a stop or adds to a loser, events when no thesis was given, and an R multiple quoted when there is no target.
Worked examples - every gate
These are the free samples on the page: the exact input the page sends and the reply that replays for free. The prices are made up for the samples. Together they cover every gate: take, resize, wait and skip.
A breakout, inside the limits - take it, size 75
Input:
{
"task": "check",
"symbol": "AMD",
"side": "long",
"currency": "USD",
"account": "60000",
"entry": "152.40",
"stop": "144.90",
"target": "172.00",
"multiplier": "1",
"planned_size": "75",
"group": "semis",
"atr": "4.10",
"win_rate": "45",
"avg_win_r": "2.1",
"limits": "risk 1%, heat 6%, group 3%, position 25%, step 1",
"open_book": "NVDA long 40 118.5 112 now 124 #semis\nXLE long 120 88.2 85.1\nJPM long 30 241 233.5 #banks",
"thesis": "Daily breakout above a seven-week base at 150 on volume 1.8x the 50-day average. Stop under the base low and the 20-day line. Earnings are in 11 trading days; I want to be at breakeven or out before then. Wrong if it closes back inside the base.",
"gate": "take",
"facts": "Trade: long AMD, entry 152.40, stop 144.90 (7.50 away, 4.92%, 1.83 ATR), target 172.00.\nRisk per unit: $7.55 (includes 0.05 slippage).\nLimits: risk per trade 1.00% of $60,000.00 = $600.00; open heat limit 6.00% = $3,600.00; group limit 3.00%; position limit 25% of the account; size step 1.\nSizes: by risk 79, by position limit 98, by heat room 334, by #semis group room 174. Maximum size: 79 (set by your risk per trade).\nHalf size (for a resize under rule 3): 39, risk $294.45 = 0.49%; open heat after it $1,371.45 = 2.29%; #semis after it $774.45; reward at target $764.40.\nAt the maximum size: risk $596.45 = 0.99% of the account, position value $12,039.60 = 20.1%, reward at target $1,548.40.\nPlanned size: 75, which risks $566.25 = 0.94% of the account - inside the maximum; at the planned size open heat after this trade would be $1,643.25 = 2.74%, leaving $1,956.75, and the reward at target $1,470.00.\nReward to risk: 2.59R after slippage; break-even win rate 27.9% (fees included).\nYour record: win rate 45%, average winner 2.10R (record expectancy 0.40R a trade); this trade's expectancy at your win rate 0.62R.\nOpen book (3 positions): open risk $1,077.00 = 1.80%; heat room $2,523.00; after this trade at the maximum size $1,673.45 = 2.79%, leaving $1,926.55 of room.\n - NVDA long 40 from 118.5, stop 112, now 124, #semis: open risk $480.00 (from now 124 to the stop).\n - XLE long 120 from 88.2, stop 85.1: open risk $372.00 (from entry 88.2 to the stop).\n - JPM long 30 from 241, stop 233.5, #banks: open risk $225.00 (from entry 241 to the stop).\nGroup #semis: open $480.00, after this trade $1,076.45, limit $1,800.00.\nLosing streaks at 1.00% a trade: 3 in a row -3.0%, 5 in a row -4.9%, 8 in a row -7.7%, 10 in a row -9.6%; at your win rate expect a run of about 8 losses somewhere in 100 trades.\n1R levels: +1R 159.90, +2R 167.40, +3R 174.90.\nGate: take - inside every limit."
}
Reply:
{
"task": "check",
"headline": "Take 75 shares, since size, stop, and heat all sit inside the limits and the earnings in 11 days is already covered by your own exit plan.",
"verdict": "take",
"size": 75,
"size_why": "Your planned 75 shares stays inside the 79-share maximum set by your risk per trade (1% = $600.00), so the planned size stands without adjustment.",
"stop": {
"read": "sound",
"note": "The stop at 144.90 sits under the base low and the 20-day line - past 150, the breakout level where the thesis says a close back inside the base proves it wrong - and at 1.83 ATR it's also inside the normal band."
},
"target": {
"note": "Target 172.00 gives 2.59R reward to risk after slippage, between +2R (167.40) and +3R (174.90)."
},
"edge": "At 2.59R reward to risk you only need a 27.9% win rate to break even; your actual record is 45% with a 2.10R average winner (0.40R record expectancy), putting this trade's expectancy at 0.62R.",
"book": "Open risk is $1,077.00 (1.80%) before this trade; at your planned 75 shares it rises to $1,643.25 (2.74%), leaving $1,956.75 of the $3,600.00 heat limit. The #semis group sits at $480.00 now; facts put it at $1,076.45 even at the full 79-share maximum, well under the $1,800.00 group limit, so 75 shares leaves more room still. NVDA carries the same #semis tag as this trade - worth checking it isn't the same semiconductor move doubled up; XLE and JPM share no tag or thesis link with this one.",
"events": [
"Earnings in 11 trading days - the point by which the thesis plans to be flat or at breakeven"
],
"plan": [
{
"when": "price reaches +1R (159.90)",
"do": "move the stop to entry (152.40)"
},
{
"when": "price reaches +2R (167.40)",
"do": "trail the stop up to +1R (159.90)"
},
{
"when": "11 trading days pass and earnings approach",
"do": "be at breakeven or flat, as the thesis plans, regardless of where price sits"
},
{
"when": "price reaches the target (172.00)",
"do": "take profit on the position"
}
],
"before_you_click": [
"Confirm the entry is still near 152.40 - the size and risk math assume this price",
"Place the stop at 144.90, under both the base low and the 20-day line",
"Confirm the earnings date is 11 trading days out and set a reminder to be flat or breakeven by then",
"Check NVDA's #semis exposure against this one before adding it",
"Confirm your broker shows open risk at $1,077.00 and #semis at $480.00 before this trade"
],
"wrong_if": "Closes back inside the base, per the thesis."
}
Too many shares - resize it, size 378
Input:
{
"task": "check",
"symbol": "SOFI",
"side": "long",
"currency": "USD",
"account": "25000",
"entry": "14.20",
"stop": "13.55",
"target": "16.10",
"multiplier": "1",
"planned_size": "1000",
"group": "none",
"atr": "0.48",
"win_rate": "50",
"avg_win_r": "1.6",
"limits": "risk 1%, heat 6%, group 3%, position 25%, step 1",
"open_book": "KO long 50 70.1 67.8",
"thesis": "Gap-fill setup after a dip to the 50-day. I am very confident on this one so I want a full-size position, 1,000 shares. Wrong if it loses 13.55.",
"gate": "resize",
"facts": "Trade: long SOFI, entry 14.20, stop 13.55 (0.65 away, 4.58%, 1.35 ATR), target 16.10.\nRisk per unit: $0.66 (includes 0.01 slippage).\nLimits: risk per trade 1.00% of $25,000.00 = $250.00; open heat limit 6.00% = $1,500.00; group limit 3.00%; position limit 25% of the account; size step 1.\nSizes: by risk 378, by position limit 440, by heat room 2,098. Maximum size: 378 (set by your risk per trade).\nHalf size (for a resize under rule 3): 189, risk $124.74 = 0.50%; open heat after it $239.74 = 0.96%; reward at target $359.10.\nAt the maximum size: risk $249.48 = 1.00% of the account, position value $5,367.60 = 21.5%, reward at target $718.20.\nPlanned size: 1,000, which risks $660.00 = 2.64% of the account - over the maximum.\nReward to risk: 2.86R after slippage; break-even win rate 25.9% (fees included).\nYour record: win rate 50%, average winner 1.60R (record expectancy 0.30R a trade); this trade's expectancy at your win rate 0.93R.\nOpen book (1 position): open risk $115.00 = 0.46%; heat room $1,385.00; after this trade at the maximum size $364.48 = 1.46%, leaving $1,135.52 of room.\n - KO long 50 from 70.1, stop 67.8: open risk $115.00 (from entry 70.1 to the stop).\nGroup: none tagged for this trade.\nLosing streaks at 1.00% a trade: 3 in a row -3.0%, 5 in a row -4.9%, 8 in a row -7.7%, 10 in a row -9.6%; at your win rate expect a run of about 7 losses somewhere in 100 trades.\n1R levels: +1R 14.85, +2R 15.50, +3R 16.15.\nGate: resize - the planned 1,000 is more than the 378 the limits allow (it risks $660.00, 2.64%)."
}
Reply:
{
"task": "check",
"headline": "Resize to 378 shares - the 1,000 you planned risks 2.64%, well over the 1% per-trade limit.",
"verdict": "resize",
"size": 378,
"size_why": "The $250.00 risk-per-trade limit (1.00% of $25,000.00) caps size at 378 units, risking $249.48 (1.00%); the 1,000 shares you planned would have risked $660.00 (2.64%), over the limit. Feeling very confident on a setup is exactly when a limit like this earns its keep.",
"stop": {
"read": "sound",
"note": "The thesis says 'Wrong if it loses 13.55' - the same price as the stop, and facts put it at 1.35 ATR, not tight itself. The stop sits right at the level that proves the idea wrong, so it reads sound."
},
"target": {
"note": "Target 16.10 gives 2.86R reward-to-risk after slippage, worth $718.20 against $249.48 at risk at the 378-share size."
},
"edge": "Reward to risk is 2.86R after slippage, above the 25.9% break-even win rate; at your 50% win rate this trade's expectancy is 0.93R, above your overall record expectancy of 0.30R a trade.",
"book": "Open heat is $115.00 (0.46%) before this trade, with $1,385.00 of room under the $1,500.00 limit; at 378 shares heat rises to $364.48 (1.46%), leaving $1,135.52 of room. No group tag here, and KO doesn't look like the same bet as this trade.",
"events": [],
"plan": [
{
"when": "price reaches +1R (14.85)",
"do": "move the stop to breakeven (14.20)"
},
{
"when": "price reaches +2R (15.50)",
"do": "trail the stop up to +1R (14.85)"
},
{
"when": "price reaches the target (16.10)",
"do": "close the position and take the 2.86R gain"
}
],
"before_you_click": [
"Confirm the order is sized at 378 shares, not the 1,000 originally planned.",
"Place the stop at 13.55 (1.35 ATR, 0.65 below entry) with the entry.",
"Check SOFI is still trading near 14.20 so the risk per share hasn't drifted from $0.66.",
"Confirm the open KO position ($115.00 risk) hasn't changed since these numbers were computed.",
"Re-check the gap-fill setup off the 50-day is still intact before clicking."
],
"wrong_if": "Wrong if SOFI loses 13.55 - the level the thesis names and where the stop sits."
}
Futures into a full book - wait, size 0
Input:
{
"task": "check",
"symbol": "MES",
"side": "long",
"currency": "USD",
"account": "40000",
"entry": "5812.25",
"stop": "5790.00",
"target": "5860.00",
"multiplier": "5",
"planned_size": "2",
"group": "index",
"atr": "18.5",
"win_rate": "48",
"avg_win_r": "1.9",
"limits": "risk 1%, heat 6%, group 3%, position 100%, step 1",
"open_book": "MNQ long 2 20410 20300 x2 #index\nAAPL long 100 228.5 221 #tech\nMSFT long 40 431 418 now 436 #tech\nGLD long 30 241 232\nMCL short 2 71.4 73.2 x100",
"thesis": "Opening-range breakout on the micro S&P after a higher low at 5790. Target is the prior day high. Wrong below 5790.",
"gate": "wait",
"facts": "Trade: long MES (multiplier 5), entry 5812.25, stop 5790.00 (22.25 away, 0.38%, 1.20 ATR), target 5860.00.\nRisk per unit: $112.50 (includes 0.25 slippage); round-trip commission $2.50.\nLimits: risk per trade 1.00% of $40,000.00 = $400.00; open heat limit 6.00% = $2,400.00; group limit 3.00%; position limit 100% of the account; size step 1.\nSizes: by risk 3, by position limit 1, by heat room 0, by #index group room 6. Maximum size: 0 (set by the heat left in the book).\nAt the maximum size: risk $0.00 = 0.00% of the account, position value $0.00 = 0.0%, reward at target $0.00.\nPlanned size: 2, which risks $227.50 = 0.57% of the account - over the maximum.\nReward to risk: 2.11R after slippage; break-even win rate 32.1% (fees included).\nYour record: win rate 48%, average winner 1.90R (record expectancy 0.39R a trade); this trade's expectancy at your win rate 0.49R.\nOpen book (5 positions): open risk $2,540.00 = 6.35%; heat room $0.00 (the book is already $140.00 over its heat limit); after this trade at the maximum size $2,540.00 = 6.35%, leaving $0.00 of room.\n - MNQ long 2 x2 from 20410, stop 20300, #index: open risk $440.00 (from entry 20410 to the stop).\n - AAPL long 100 from 228.5, stop 221, #tech: open risk $750.00 (from entry 228.5 to the stop).\n - MSFT long 40 from 431, stop 418, now 436, #tech: open risk $720.00 (from now 436 to the stop).\n - GLD long 30 from 241, stop 232: open risk $270.00 (from entry 241 to the stop).\n - MCL short 2 x100 from 71.4, stop 73.2: open risk $360.00 (from entry 71.4 to the stop).\nGroup #index: open $440.00, after this trade $440.00, limit $1,200.00.\nLosing streaks at 1.00% a trade: 3 in a row -3.0%, 5 in a row -4.9%, 8 in a row -7.7%, 10 in a row -9.6%; at your win rate expect a run of about 7 losses somewhere in 100 trades.\n1R levels: +1R 5834.50, +2R 5856.75, +3R 5879.00.\nGate: wait - open risk $2,540.00 (6.35%) leaves no room under the 6.0% heat limit ($2,400.00); the planned 2 is more than the 0 the limits allow (it risks $227.50, 0.57%)."
}
Reply:
{
"task": "check",
"headline": "Wait, size 0 - the open book is already $140.00 over its 6.00% heat limit, leaving no room for this MES trade.",
"verdict": "wait",
"size": 0,
"size_why": "Maximum size is 0, set by the heat left in the book: open risk is already $2,540.00 (6.35%), $140.00 over the $2,400.00 (6.00%) heat limit, so there is $0.00 of room no matter what the $400.00 per-trade cap or the #index group's $1,200.00 limit would otherwise allow.",
"stop": {
"read": "sound",
"note": "The stop at 5790.00 is 1.20 ATR away and sits exactly at the level the thesis calls wrong-if ('higher low at 5790 ... Wrong below 5790'), so it's placed where the idea itself is disproven."
},
"target": {
"note": "Target 5860.00 is the prior day high per the thesis, giving 2.11R reward to risk after slippage."
},
"edge": "Reward to risk is 2.11R after slippage, with a 32.1% break-even win rate; at your 48% record this trade's expectancy works out to 0.49R, above your 0.39R average per-trade expectancy.",
"book": "The book is already $140.00 over its heat limit - open risk is $2,540.00 (6.35%) against the $2,400.00 (6.00%) cap - so no new risk can go on until it's back under that; at the maximum size of 0, this trade leaves it exactly where it is, $2,540.00 (6.35%) with $0.00 of room. The #index group sits at $440.00 of its $1,200.00 limit, all from the open MNQ long tagged #index - the same bet tag as this trade. No other open position shares a tag or a stated link to this one.",
"events": [],
"plan": [
{
"when": "open book heat drops back under the $2,400.00 (6.00%) limit (something in the book closes or is trimmed)",
"do": "that is the only thing that frees up size for this trade - re-run the check once it does"
},
{
"when": "the check is re-run",
"do": "treat it as a fresh read - do not reuse today's $0 maximum or the planned size of 2 without new numbers"
}
],
"before_you_click": [
"Confirm total open heat is back under the $2,400.00 (6.00%) limit before treating this as live again.",
"Re-run the check for a fresh maximum size and risk numbers rather than reusing today's.",
"Check the #index group still has room under its $1,200.00 limit for whatever size comes back.",
"Make sure the 5790 higher-low setup and the prior-day-high target are still in place.",
"Keep the stop at 5790.00, the thesis's wrong-if level - do not widen it to fit a size."
],
"wrong_if": "Wrong below 5790.00, per the thesis."
}
A short that does not pay - skip it, size 0
Input:
{
"task": "check",
"symbol": "SAP",
"side": "short",
"currency": "EUR",
"account": "30000",
"entry": "236.50",
"stop": "243.00",
"target": "231.00",
"multiplier": "1",
"planned_size": "40",
"group": "none",
"atr": "5.20",
"win_rate": "40",
"avg_win_r": "1.5",
"limits": "risk 1%, heat 6%, group 3%, position 25%, step 1",
"open_book": "ASML long 6 652 628\nSIE long 20 214.5 207",
"thesis": "Fading the rally into the gap from last month. Resistance around 238. Target is the 20-day line at 231. Wrong above 243.",
"gate": "skip",
"facts": "Trade: short SAP, entry 236.50, stop 243.00 (6.50 away, 2.75%, 1.25 ATR), target 231.00.\nRisk per unit: €6.60 (includes 0.10 slippage); round-trip commission €9.80.\nLimits: risk per trade 1.00% of €30,000.00 = €300.00; open heat limit 6.00% = €1,800.00; group limit 3.00%; position limit 25% of the account; size step 1.\nSizes: by risk 43, by position limit 31, by heat room 226. Maximum size: 31 (set by the position-size limit).\nHalf size (for a resize under rule 3): 15, risk €108.80 = 0.36%; open heat after it €402.80 = 1.34%; reward at target €72.70.\nAt the maximum size: risk €214.40 = 0.71% of the account, position value €7,331.50 = 24.4%, reward at target €160.70.\nPlanned size: 40, which risks €273.80 = 0.91% of the account - over the maximum.\nReward to risk: 0.82R after slippage; break-even win rate 57.6% (fees included).\nYour record: win rate 40%, average winner 1.50R (record expectancy 0.00R a trade); this trade's expectancy at your win rate -0.32R.\nOpen book (2 positions): open risk €294.00 = 0.98%; heat room €1,506.00; after this trade at the maximum size €508.40 = 1.69%, leaving €1,291.60 of room.\n - ASML long 6 from 652, stop 628: open risk €144.00 (from entry 652 to the stop).\n - SIE long 20 from 214.5, stop 207: open risk €150.00 (from entry 214.5 to the stop).\nGroup: none tagged for this trade.\nLosing streaks at 1.00% a trade: 3 in a row -3.0%, 5 in a row -4.9%, 8 in a row -7.7%, 10 in a row -9.6%; at your win rate expect a run of about 9 losses somewhere in 100 trades.\n1R levels: +1R 230.00, +2R 223.50, +3R 217.00.\nGate: skip - at a 40% win rate a 0.82R winner loses 0.32R a trade on average; the planned 40 is more than the 31 the limits allow (it risks €273.80, 0.91%)."
}
Reply:
{
"task": "check",
"headline": "Skip, size 0 - at your 40% win rate this 0.82R short averages -0.32R a trade, a losing edge no size fixes (the planned 40 also topped the 31-unit position-size limit).",
"verdict": "skip",
"size": 0,
"size_why": "Size is 0: at a 40% win rate this 0.82R trade averages -0.32R, a losing edge that no size corrects - the planned 40 was also above the 31-unit maximum set by the position-size limit, but that's beside the point now.",
"stop": {
"read": "sound",
"note": "The thesis says the idea is wrong above 243.00, and the stop sits right there, 1.25 ATR away - too far from 1 ATR to be tight, nowhere near 3 to be wide."
},
"target": {
"note": "The target is 231.00, the thesis's 20-day line, giving 0.82R reward to risk after slippage."
},
"edge": "Reward to risk is 0.82R after slippage against a 57.6% breakeven win rate; at your actual 40% win rate (1.50R average winner) this trade's expectancy is -0.32R.",
"book": "Since this trade isn't taken, open heat stays at €294.00 = 0.98%, with €1,506.00 of room untouched. No group is tagged. ASML (long 6 from 652) and SIE (long 20 from 214.5) are both long and unrelated to this short SAP idea - nothing here looks like the same bet.",
"events": [],
"plan": [
{
"when": "Your record win rate on this kind of short fade climbs toward the 57.6% breakeven (it's 40% now)",
"do": "Re-run the page then - the edge has to turn positive, not just the size."
},
{
"when": "Your average winner on this setup grows well past 1.50R, enough to clear breakeven at a 40% win rate",
"do": "Re-run the page before sizing it - a bigger average win is what would fix the expectancy, not a smaller position."
}
],
"before_you_click": [
"Re-run the page with your current record rather than assuming the edge has changed.",
"Confirm the thesis still treats 243.00 as the level that proves it wrong before using that stop again.",
"Check what the position-size limit then allows before sizing - it capped this one at 31 against a planned 40.",
"Confirm ASML and SIE are still open at the stops shown before adding new risk."
],
"wrong_if": "The thesis says the idea is wrong if price trades above 243.00, the same level as the stop."
}
Build the input yourself
Load the engine in node (global.window = global; require("./tradekit.js"); require("./replykit.js")), then:
const K = TradeKit, R = ReplyKit;
const built = K.buildInput(K.form({ symbol: "AMD", side: "long", entry: "152.40", stop: "144.90", target: "172.00",
equity: "60000", currency: "USD", planned: "75", win: "45", avgwin: "2.1", group: "semis", atr: "4.10",
book: "NVDA long 40 118.50 112.00 now 124.00 #semis\nXLE long 120 88.20 85.10",
thesis: "Breakout above a seven-week base. Wrong if it closes back inside the base." })); // built.ok is false, with built.note, when the stop is missing or on the wrong side
const wire = K.toWire(built.input); // POST this to /estimate and /run; built.facts holds every sum, built.facts.gate the gate
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes // the page's re-check of the reply
// R.md(built, reply, K.title(built.form)) // the sizing, the book and the read as Markdown
Form keys: symbol, side, entry, stop, target, mult, planned, group, atr, slip, comm, equity, currency, win, avgwin, book, thesis and limits (risk, heat, group, position, step).
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://before-the-fill.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://before-the-fill.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// before-the-fill.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://before-the-fill.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://before-the-fill.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// BeforeTheFill.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class BeforeTheFill {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://before-the-fill.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://before-the-fill.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://before-the-fill.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://before-the-fill.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"before-the-fill"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "before-the-fill"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "before-the-fill" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "before-the-fill"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "before-the-fill"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "before-the-fill" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "before-the-fill"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "before-the-fill" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "check", "symbol": "AMD", "side": "long", "account": "60000", "entry": "152.40", "stop": "144.90", "...": "...", "gate": "take", "facts": "...", ...}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "check", "symbol": "AMD", "side": "long", "account": "60000", "entry": "152.40", "stop": "144.90", "...": "...", "gate": "take", "facts": "...", ...}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "check", symbol, side, account, entry, stop, ..., gate, facts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses before-the-fill:<task>:<inputHash>:a<attempt> (the one task is check), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="before-the-fill:check:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "before-the-fill:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"), reply.get("size"), reply.get("stop"))
import { createHash } from "node:crypto";
const key = `before-the-fill:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict, reply.size, reply.stop);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "before-the-fill:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "before-the-fill:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "before-the-fill:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"], reply["size"], reply["stop"]].inspect
$key = "before-the-fill:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), " ", ($reply["size"] ?? ""), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"before-the-fill:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: before-the-fill:check:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "check", "symbol": "AMD", "side": "long", "account": "60000", "entry": "152.40", "stop": "144.90", "...": "...", "gate": "take", "facts": "...", ...}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "before-the-fill:check:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `before-the-fill:check:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "before-the-fill:check:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "before-the-fill:check:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "before-the-fill:check:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: before-the-fill:check:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"before-the-fill:check:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);