Details
Maker-Tight Election, click by click
Every screen a user touches between choosing a template and arming it, built out rather than described. Click the steps. Step 3 has a screen per bot — that is the part worth arguing about.
This is a design spec, not a working builder: the controls are positioned, not wired. What matters is the shape — which questions get asked, in what order, with what consequence shown alongside.
Pick a template
The user has not chosen a market, a size, or a bot yet. They are choosing an intention. Every card leads with what the strategy is trying to do, and is honest about how much it will ask of them.
Maker-Tight Election — what this template has already decided for you
Shown so the user knows what they are inheriting. These are not editable in the builder; changing them means cloning the template.
| Engine | strat.maker_tight — quotes both sides, never takes a directional view |
| Always-on guards | risk.killswitch, risk.compliancegate, risk.markethaltguard — you cannot switch these off |
| Requote triggers | On mid move and on depth change. Not on time. |
| Settlement asset | pUSD only. A market that settles in anything else is skipped, with DISC_ASSET_MISMATCH. |
Note what is missing: no code editor, no file upload, no “paste your strategy” box.
The card copy is not marketing text — it is the template’s summary field and it is required to be one sentence with no jargon. Review rejects templates whose summary needs a second sentence, on the grounds that a strategy you cannot state in one line is a strategy the user cannot supervise.
“What this template has already decided for you” renders every manifest entry marked locked: true. Locked entries are shown, never hidden. Hiding them is how you get a user who believes their strategy is doing something it is not.
The decision count on each card is computed, not authored: it is the number of manifest entries with locked: false. If a template drifts past eight, CI fails it.
Pick your markets
The template arrived bound to a cluster. The user narrows it. The two unselected rows are not hidden — they are shown greyed with the reason, because “why isn’t my market here” is the single most common support question on every trading product ever built.
| Market | Cluster | Bid / ask | 24h vol | |
|---|---|---|---|---|
| Will the Democratic nominee win Pennsylvania? | US_ELECT_2028_A | 41¢ / 43¢ | 18.2k | |
| Will the Republican nominee win Pennsylvania? | US_ELECT_2028_A | 57¢ / 59¢ | 17.9k | |
| Will turnout exceed 62%? | US_ELECT_2028_A | 33¢ / 36¢ | 4.1k | |
| Will there be a recount in any state? | US_ELECT_2028_A | 12¢ / 17¢ | 0.9k | |
| Will the Senate flip? spread too wide | US_ELECT_2028_A | 48¢ / 52¢ | 0.3k | |
| Will a third-party candidate exceed 5%? spread too wide | US_ELECT_2028_A | 7¢ / 19¢ | 0.1k |
Recount has traded 0.9k in 24h. Below 1k the quality guard will pull your quotes most of the day. Keep it if you want, but expect RISK_SPREAD_WIDE in the log.
The estimate updates as rows are ticked, so “select all 50” visibly turns into “3,000 envelopes a day” before it turns into a bill.
Ineligible markets stay in the table, greyed, carrying the reason code that excluded them. This costs a row of vertical space and saves a support queue — “why isn’t my market here” is the most common question on every trading product ever shipped.
The envelope estimate comes from the template’s measured envelopes-per-market-per-day multiplied by the selection, not from a formula in the front end. When we change engine behaviour the estimate moves on its own.
The 50-market cap is enforced in the manifest schema, not in the UI. The counter is a courtesy; the schema is the rule. Never let a limit live only in a form.
Tune each bot, on a screen built for that bot
This is where a generic form would have put twelve number fields in two columns. Instead every bot gets its own screen, and the controls are chosen to match what the bot actually decides. An engine that picks prices gets a price ladder. A guard that refuses things gets a sentence and a capacity bar. A router that trades speed against price gets three named choices, not fourteen sliders.
Click through all four. The pattern is deliberately identical every time: the question in plain words, the control, then a green line saying what the answer means, then an amber line if the answer is risky. Developers can build any new bot’s screen from that skeleton.
How tight do you want to quote?
This is the only decision that makes you money. Quote tight and you trade a lot for a thin margin. Quote wide and you trade rarely for a fat one. There is no right answer, so we show the consequence instead of the number.
Pick a starting point
Then drag the handles if you want something in between. Most users never drag.
Or set it by hand
The market is currently trading at 42¢. Drag either handle.
You will offer to buy at 41¢ and sell at 43¢, in 200 pUSD clips, and you will not change your mind more than once every 10 seconds.
How much per clip?
The size of each individual offer. Not your total exposure — the guard on the next screen handles that.
At 200 pUSD a clip and a 500 pUSD cap per market, you can be filled 2 and a half times on one market before you stop quoting that side.
Raise this above 400 and a single fill uses most of your per-market room. The engine will spend more of the day flat than quoting.
What this screen writes into the bundle
engine strat.maker_tight { half_spread_bps = 10 // from the handles: 1c on a 42c mid quote_size_pusd = 200 // from the slider cooldown_s = 10 // preset-derived, not user-facing requote_on = [mid, depth] // fixed by the template }
How much are you willing to have riding at once?
A guard has no opinion about the market. It only ever says no. So its screen is not a form — it is a sentence the user completes, with a picture of what that sentence means against the money they actually have.
Complete the sentence
This is the whole configuration for this bot. Two numbers.
or more than pUSD across .
Once you hold 500 pUSD of one market, new buy orders on that market are rejected with RISK_EXPOSURE_CAP_HIT. Sells still go through — a guard never traps you in a position.
What should happen when you hit the ceiling?
Keep quoting the side that gets me out
You stop adding, but you keep offering to sell. This is how the position unwinds on its own.
If off: you go completely silent on that market and wait for it to resolve.
Put it in my attention queue
First time it happens each day. Not every rejection — that would be hundreds.
If off: it is still in the log, you just will not be told.
Pause the whole strategy, not just that market
Blunt. Use it if hitting a cap at all means your assumptions were wrong.
Off is the default. Most people want the other three markets to keep working.
What this screen writes into the bundle
guard risk.exposureguard { max_pos_market = 500 max_pos_cluster = 5000 scope = cluster:"US_ELECT_2028_A" on_breach = { allow_reduce: true, notify: once_daily, halt: false } }
When you do trade, what matters more?
Routing has about fourteen tunable parameters. Thirteen of them are trade-offs along one axis: speed versus price. So we expose the axis, not the parameters. An advanced user can open the drawer and set all fourteen.
Pick one
On last week’s data, Balanced on these four markets would have filled 131 of 140 intents, and the nine misses were all in the last hour before close.
Where are you allowed to trade?
Polymarket CLOB
The main order book. Deepest, and where the template expects to work.
CTFExchangeV2
The V2 contract. Occasionally a better price. Adds about 40ms.
If off: you give up roughly 0.2¢ a fill on this cluster.
All fourteen routing parameters (advanced)
Opening this drawer switches the strategy to manual routing and marks it for review before it can leave shadow. The three cards above are the reviewed paths.
route exec.smartrouter { objective = balanced // set by the card max_latency_ms = 200 rest_first_ms = 200 cross_after_ms = 200 venues = [clob, ctfv2] max_child_orders = 3 min_child_pusd = 50 cancel_on_reject = true // + 6 more, all derived from `objective` }
When should this stop for the day?
One number. The screen’s job is to make the number feel like money rather than a percentage, and to say out loud what happens at the moment it trips.
Stop for the day if this strategy is down
On a 5,000 pUSD ceiling that is 400 pUSD. In the last 90 days this strategy would have stopped 3 times, and on 2 of those days it would have finished green if left alone.
Below 4% you will stop on ordinary noise. The election cluster moves 3–5% intraday without anything happening.
When it trips
Cancel everything resting
Pull your quotes immediately. Takes about 300ms.
Also sell out of what I am holding
Off by default. Selling into the move that just hurt you usually makes it worse.
If off: you keep the position and it resolves on its own.
Start again automatically tomorrow
At 00:00 UTC. If off, it waits for you to press resume.
What this screen writes into the bundle
guard risk.drawdownguard { daily_stop_pct = 8 scope = strategy on_trip = { cancel_resting: true, flatten: false, auto_resume: "00:00Z" } }
Four bots, six questions between them. The other 23 parameters in the bundle are template defaults, shown read-only in the review step.
The builder has no per-bot code. It renders whatever the bot’s decision manifest declares, using the control vocabulary. If you find yourself wanting a special case in the builder for your bot, the manifest is missing an expression — add it to the vocabulary and everyone benefits.
Every control declares a consequence expression (the green line) and optionally a risk_when predicate with its own copy (the amber line). Both are evaluated client-side against the current draft, so they update as the user drags. A control with no consequence expression will not pass review.
Numbers shown in consequence lines that come from measurement — fills per day, average slippage, “fired 9 times last week” — are read from the last completed replay for that cluster, cached daily. They are labelled as observations, never as predictions, and when no replay exists the line is omitted rather than guessed.
The escape-hatch drawer at the bottom of each screen is generated by serialising exactly the fields this screen owns. It is not a hand-written sample, so it cannot drift from what the screen actually writes.
Read it back in English
Before anyone is allowed to run anything, the machine says the whole strategy back as prose. If the user cannot recognise their own intention in this paragraph, the builder failed. Every highlighted value links back to the control that produced it.
On the
You will never hold more than
When you do need to trade, you will rest the order for
If this strategy is down
Three guards you did not configure are always on and cannot be turned off: the kill switch, the compliance gate, and the market-halt guard. always-on
The readback is a hard gate, not a nicety. Every manifest entry must supply a say_template, and the build fails without one. That constraint is doing real design work: it makes it impossible to expose a parameter we cannot explain, which is the failure mode that produced the old strategy-detail page.
Sentences are grouped by owning bot and tagged with the bot slug, so a user who wants to know which component is responsible for a clause can see it without us adding a legend.
The always-on paragraph is rendered from the platform policy set rather than the bundle, and it is not suppressible. Users should never be surprised by a guard they did not know existed.
The full bundle, all 29 parameters
The six you chose are marked. The rest come from the template. This is also the download link, the git diff, and what gets attached to the review ticket — the DSL never went away, it just stopped being something a person has to type.
// maker-tight-election.bundle · draft · never executed bind cluster "US_ELECT_2028_A" markets [pa-dem, pa-rep, turnout-62, recount-any] // <- you engine strat.maker_tight { half_spread_bps = 10 // <- you (Balanced) quote_size_pusd = 200 // <- you cooldown_s = 10 requote_on = [mid, depth] min_book_depth = 1000 skew_on_inv = true } guard risk.exposureguard { max_pos_market = 500 // <- you max_pos_cluster = 5000 // <- you allow_reduce = true } guard risk.drawdownguard { daily_stop_pct = 8 // <- you flatten = false, auto_resume = "00:00Z" } route exec.smartrouter { objective = balanced // <- you venues = [clob, ctfv2], max_latency_ms = 200 } always risk.killswitch, risk.compliancegate, risk.markethaltguard rung demo-wired builder_code "0xPT…a91c"
Dry run, then arm — if you are allowed to
The strategy is replayed against the last seven days of real recorded market data. Nothing is placed. The user sees what it would have done and, more importantly, every time a guard would have said no.
It ran the whole week without breaking anything
140 intents, 131 filled, 9 rejected by guards you configured. No always-on guard ever fired, which means the strategy never tried to do anything structurally forbidden. Modelled result: +218 pUSD on a 5,000 pUSD ceiling — but see the caveat below.
| Reason code | Count | What happened | What we’d do |
|---|---|---|---|
| RISK_EXPOSURE_CAP_HIT | 14 | You hit the 500 pUSD per-market lid on pa-dem four days running, always in the last hour. | Expected. Raise the cap or accept the flat hour. |
| RISK_SPREAD_WIDE | 61 | The recount-any market was too illiquid to quote for most of the week. | Consider dropping that market. |
| EXEC_ROUTE_CTFV2_ACCEPTED | 38 | 27% of fills went through V2 rather than the main book. | Working as intended. |
| INTEL_DEPTH_THIN | 9 | Top-of-book depth fell below the quoting threshold nine times. | Normal for overnight hours. |
| RISK_DRAWDOWN_TRIGGERED | 0 | Never fired. The worst day was −3.1%. | Your 8% stop was never close. |
The caveat, shown at the same size as the result. A replay assumes your order would have been filled at the recorded price without moving it. On thin markets that is generous. Treat +218 as the ceiling of what was possible, not a forecast.
Seven days, no engine errors.
All four are demo-wired. A template containing a frozen bot cannot be armed at all.
Not started. Shadow means it watches real markets and places nothing. This is rung 3 of the ladder and there is no way to skip it.
Requires the shadow report. You are Admin, so you can sign your own — but not before the 48h.
The disabled button always says why it is disabled. A greyed control with no explanation is a support ticket.
The replay runs the real engine against recorded envelopes — same binary, same guards, replayed clock. It is not a separate simulator, because a separate simulator eventually disagrees with production and then nobody trusts either one.
The caveat is rendered at the same weight as the headline number and cannot be dismissed. Fill assumptions on thin books are optimistic, and a product that quietly implies otherwise is mis-selling. If you are tempted to shrink that box, you are building the wrong thing.
The four gate rows come from the same gate definitions the pipeline page renders. A gate cannot be described one way in the builder and another way on the ladder — there is one definition and both surfaces read it.
The arm button is disabled by the gate evaluator returning an unmet-requirement list, and the adjacent text is that list’s first entry. There is no code path that disables the button without producing a reason.