PitBridge
Join the waitlist

When people say an AI agent has guardrails, they usually mean it was told to behave. It was given a system prompt with rules, maybe a stern paragraph about risk. That is worth doing, but it is not a control. A control is something the agent cannot override even when it decides to, or is tricked into trying. On a trading account, that distinction is the whole game.

Why a prompt limit is not a control

Suppose your system prompt says never exceed two contracts and never add after a daily loss of five hundred dollars. The model reads that. Most of the time it complies. But a language model produces a distribution of outputs, and the tail of that distribution includes a place_order for five contracts. It can get there through a plain mistake, a misread of the current position, an unusual market that pushes it off its training distribution, or an input crafted to talk it past its instructions. When that call is generated, a prompt limit does nothing, because the prompt was never the thing standing between the order and the account. It was a suggestion the model was free to follow.

This is not a theoretical worry. Prompt injection is now routinely described as the new SQL injection, and for the same reason: untrusted text and trusted instructions travel in the same channel, and the model has no reliable way to tell them apart. Every prompt-hardening mitigation is probabilistic, and researchers publish fresh bypasses within weeks of each new defense. The deeper issue is one of layers. You cannot parameterize a prompt the way you parameterize a SQL query, so a system prompt is simply the wrong place to put a limit that must always hold. Enforcement has to live below the model, in code the model cannot rewrite.

What makes a control enforceable

A guardrail is enforceable when three things are true. It runs in a separate process, so it is not part of the context the model can reason around. It is deterministic, so the same order and the same config always produce the same decision, with no model in the adjudication. And it sits after the tool call and before the platform, so there is no path to the account that skips it. The model’s only power is to propose. The daemon decides.

This is why PitBridge puts the guardrail engine in the local daemon rather than in the agent. The agent calls place_order. The call arrives at the engine. The engine checks it against your limits and returns allow or block. An allowed order continues to the platform; a blocked one stops with a reason. The model never sees around this, because the check does not live where the model lives.

The frozen pipeline: where a control actually lives

A control is only as good as its placement. Every order, whether it comes from an AI agent over MCP or from a plain HTTP request, enters through one method and travels one fixed pipeline. There is no second code path from the agent to the broker, no side door that skips the checks. The pipeline has six stages, and the first stage that refuses an order wins.

StageWhat it checksExample block code
1. SchemaRequest shape and types; quantity is an integer at least 1SCHEMA_INVALID, INVALID_QTY
2. PermissionAccount mode: read_only, paper, or live, plus the arm-live gatePERMISSION_READ_ONLY, LIVE_NOT_ARMED
3. Guardrail chainThe twelve controls, evaluated kill-switch firstKILL_SWITCH, MAX_POSITION
4. Human confirmHolds an allowed order for a person, if configuredHUMAN_CONFIRM_EVERY_LIVE_ORDER
5. Executor submitHands an allowed order to the AddOn linkLINK_SEND_FAILED
6. AuditAppends the request, decision, and outcome to a hash chainrecords every decision

Two stages before the guardrail chain already stop whole classes of bad order. The schema stage rejects a malformed request, including a quantity that is zero or negative: a negative BUY is a disguised SELL that would otherwise slip past a positive per-order cap, so it never gets the chance. The permission stage enforces the account mode. An order to a read_only account returns PERMISSION_READ_ONLY, and an attempt to route live in a build that is not armed returns LIVE_NOT_ARMED. De-risking, meaning cancel, close, and flatten, runs through the same pipeline but is deliberately not gated by the risk guardrails, because getting flat has to work even while entries are halted.

The twelve controls, by what they govern

Version zero ships twelve controls across eleven modules (the position caps live together). They are easier to reason about in three groups, by the question each one answers. Here is the full set with its real reason codes.

Guardrail (config key)GovernsReason code(s)What it blocks
kill_switchaccount stateKILL_SWITCHAll new entries while engaged. De-risking stays allowed.
link_downaccount stateLINK_DOWNAny order while the AddOn link has gone silent.
instrument_allowlistpre-tradeINSTRUMENT_NOT_ALLOWEDAny contract that is not on your allowlist.
position_capspre-tradeMAX_CONTRACTS_PER_ORDERA single order larger than the per-order cap.
position_capspre-tradeMAX_POSITIONAn order whose projected net position exceeds the cap.
trading_windowpre-tradeOUTSIDE_TRADING_WINDOW, NO_TRADE_DAYAn entry outside session hours or on a holiday or early close.
daily_loss_haltaccount stateDAILY_LOSS_HALTNew entries once day P&L hits the floor. Sticky to ET rollover.
profit_lockaccount statePROFIT_LOCKNew entries once the day is banked above your set level.
cooldownaccount stateCOOLDOWN_AFTER_LOSS, COOLDOWN_AFTER_ORDERAn entry inside the cooldown window after a loss or an order.
rate_limitaccount stateMAX_ORDERS_PER_MINUTE, MAX_ORDERS_PER_HOUR, MAX_ORDERS_PER_DAYOrders past the per-minute, hour, or day count.
duplicate_protectionaccount stateDUPLICATE_ORDERA repeat of the same account, instrument, side, and quantity in the window.
human_confirmhuman in loopHUMAN_CONFIRM_FIRST_ORDER, HUMAN_CONFIRM_EVERY_LIVE_ORDER, HUMAN_CONFIRM_SIZE_INCREASEHolds an order for a person until approved; auto-rejects after 60 s.

Every value in your config is an example you set. The names in the reason-code column are not. They are a fixed contract: the same string appears in the block result the agent reads, in the live event stream, and in the audit log, so a single decision reads the same everywhere.

Pre-trade structural checks: does this order even make sense?

These run on the order itself, independent of what happened earlier in the session. instrument_allowlist refuses any contract you did not list. The two position caps share one module: max_contracts_per_order rejects a single oversized order, and max_position rejects an order whose projected net position after the fill would exceed your cap. Because the check is on the projected position, a risk-reducing order that brings your position back toward the cap stays allowed. trading_window blocks entries outside your session hours and, through a holiday calendar that is early-close aware, on days you should not be entering at all. This is the same class of preset size, price, and instrument check that broker market-access systems have run for years before an order reaches an exchange, which is lineage, not a compliance claim.

Account-state controls: what has already happened today?

These read live account and session state. link_down blocks orders whenever the AddOn link goes silent, because queueing an order against a dead link is how you end up exposed when you thought you were covered. daily_loss_halt stops new entries once the day P&L, realized plus unrealized, reaches your floor, and with day_halt_sticky the account stays shut until the ET session rollover even if the number recovers. profit_lock banks a good day: it exists because a day run far above target can, under a firm’s consistency rule, delay an evaluation from passing, so you may want to stop while ahead. It does not lock in or guarantee any profit. cooldown enforces a pause after a loss or after any order. rate_limit caps orders per minute, hour, and day, which is what stands between you and a runaway retry loop. duplicate_protection refuses a repeat of the same account, instrument, side, and quantity inside a short window, the classic agent double-submit.

Human in the loop: should a person see this first?

human_confirm runs last, and that ordering is deliberate: a person is never paged to approve an order the engine would have refused anyway. When it is on, an allowed order is held as PENDING_CONFIRM, a notifier pings you, and you approve from the CLI. If you do nothing it auto-rejects after sixty seconds, and the daemon re-checks the kill switch, the halts, and the guardrails at confirm time, so an approval cannot resurrect an order the state has since invalidated. It has four modes.

ModeWhen it asksReason code
offNever; an allowed order flows straight to the executornone
first_orderOnly the first live order of a sessionHUMAN_CONFIRM_FIRST_ORDER
every_live_orderEvery live orderHUMAN_CONFIRM_EVERY_LIVE_ORDER
size_increaseOnly when an order increases the positionHUMAN_CONFIRM_SIZE_INCREASE
kill_switch

A single switch you control that blocks all new entries at once, whatever the agent is doing. De-risking stays allowed, so you can always get flat.

BLOCK all entries. KILL_SWITCH

instrument_allowlist

Refuses any contract that is not on the allowlist you set, before position or state is even considered.

BLOCK NQ 09-26. INSTRUMENT_NOT_ALLOWED

daily_loss_halt

Halts new entries once the day's realized and unrealized loss reaches your floor, sticky to the session rollover.

HALT day -400. DAILY_LOSS_HALT

human_confirm

Holds an allowed order for a person to approve, and auto-rejects it after sixty seconds if no one does.

HOLD 60s. HUMAN_CONFIRM_EVERY_LIVE_ORDER

None of this lives in prose. Each limit is a key in your config.toml, read at startup, and the reason code in a block is the machine-readable echo of the key that refused the order.

config.toml
[accounts.sim]
nt_account = "Sim101"
mode = "paper"          # read_only | paper | live
profile = "eval"
day_halt_sticky = true

[accounts.sim.guardrails]
instruments = ["MES 09-26"]        # allowlist: only this contract may trade
max_contracts_per_order = 5        # reject any single order larger than this
max_position = 10                  # cap the net position per instrument
daily_loss_halt = 400              # halt the day after -$400
trading_window = { tz = "America/New_York", open = "09:30", close = "16:00" }
max_orders = { per_minute = 6, per_hour = 60, per_day = 200 }
human_confirm = "first_order"      # off | first_order | every_live_order | size_increase

Example config. The keys are real; the values are yours to set. A reason code names the exact key that refused an order.

Why the order of the checks is frozen

The chain evaluates in a fixed order, and that order is part of the safety contract, not an implementation detail. kill_switch is always first: an absolute veto should short-circuit everything else, so nothing runs after it once it is engaged. human_confirm is always last, so only a fully-allowed order ever reaches a person. In between, first block wins, and the engine refuses to start if the built-in chain is missing any member, so a control cannot be silently dropped or reordered. Plugins can only be appended between the built-ins and human_confirm. They can add a check, never remove one.

The most important guardrail is the tool that isn’t there

The strongest safeguard on this list is not a check at all. It is an absence. The agent’s entire surface is nine MCP tools: five reads (get_accounts, get_positions, get_orders, get_account_state, get_guardrail_status) and four actions (place_order, cancel_order, close_position, flatten_account). There is no tool to release the kill switch, no tool to arm live trading, and no tool to change a threshold. The configuration and the kill and arm controls live on a separate operator-only CLI that the model’s context never touches.

This is the concrete meaning of a control an LLM cannot override. It is not that the model is well-behaved or well-prompted. It is that the capability to weaken its own limits is absent from its world. You can ask it to raise max_contracts to a hundred; it has no function to call.

Engaging the brakes: the kill switch and de-risking

The kill switch is file-backed. Its presence as a file under PITBRIDGE_HOME means engaged, so a person can touch that file and stop all new entries even if the daemon is wedged and answering nothing else. You can also engage it by CLI or REST. Release is CLI-only, with pitbridge unkill, and there is no remote or agent path to lift it. Under a kill, the asymmetry is the whole point: new entries return KILL_SWITCH, but cancel, close, and flatten stay allowed, so you can always get flat. The same asymmetry holds under a daily-loss halt. day_halt_sticky, on by default, keeps a halted account shut until the ET rollover, and the optional flatten_on_halt will flatten the account the moment the halt trips rather than leaving the position for you to close by hand.

~/pitbridge
# the agent proposes an order that breaks a configured limit
place_order account=sim instrument="MES 09-26" side=BUY qty=20

# the engine adjudicates. the model has no say here.
BLOCKED  reason_code=MAX_CONTRACTS_PER_ORDER
       order not sent. adjust the order or the rule.

Example decision. Field names and the reason code match the daemon; the values are yours to configure.

You can read every decision: the audit chain

Enforceability includes provability. A control you cannot inspect after the fact is a control you are asked to take on faith. Every request, decision, and outcome is written to an append-only, hash-chained JSONL log. pitbridge audit why <order_id> returns the original request, the guardrail decision with its reason code, and the submit result, so any single order can be explained without guesswork. pitbridge audit verify --anchor proves the whole chain is intact and, crucially, catches someone deleting the tail of the log, which a bare hash chain cannot see on its own. A control you cannot audit is a control you cannot trust.

~/pitbridge
# ask the log why one order stopped
pitbridge audit why ord_7f3a --log audit.jsonl

request   place_order account=sim MES 09-26 BUY qty=20
decision  BLOCK reason_code=MAX_CONTRACTS_PER_ORDER
outcome   not sent
chain     verified (hash-linked to previous entry)

Example output. Real command and reason code. Every decision, allow or block, lands in the same tamper-evident chain.

What this does not promise

Honesty about limits is part of the design. A guardrail enforces the rules you configure. It does not predict the market, it does not keep an account funded, and it does not prevent losses. An allowed order can still lose, because the market can move against a position that was fully within your limits. What the engine promises is narrow and real: an order that breaks a configured limit does not reach your account, and every decision is logged.

If you want checks like these between your agent and your account, tell us your platform on the waitlist. The full set is on the guardrails page, the pillar on trading AI guardrails covers why agents need them, and the security model explains where each part runs. PitBridge is trading infrastructure, not financial advice: it enforces the limits you configure and does not promise any trading outcome.

Read the pillar: Trading AI guardrails

Questions

What are AI trading guardrails?

They are runtime controls that check every order an agent tries to place, running in a separate process from the model. Unlike instructions in a system prompt, they are not advice the model can talk itself past. They adjudicate the order deterministically and return allow or block before anything reaches the account.

Can I not just tell the model its limits in the system prompt?

You can, and you should give it context. But a system prompt is advisory. A model can still generate an out-of-bounds tool call through a mistake, a misread, or an adversarial input. A prompt is guidance, not enforcement.

What makes a guardrail one the model cannot override?

It runs in a separate process, outside the model's context, and it adjudicates every order deterministically. The model can only propose a tool call. It has no path to change or skip the check that runs after, and its tool surface has no function to weaken a limit.

Can an agent turn its own guardrails off?

No. The agent has exactly nine tools, and none of them release the kill switch, arm live trading, or change a limit. The configuration and the kill and arm controls live on an operator-only CLI the model never touches. The operator can change a limit; the order-placing agent cannot.

Do guardrails prevent losses?

No. Guardrails enforce the limits you configure, such as a maximum position or a daily loss halt. They do not predict the market and they do not promise a result. Markets can still move against an order that was fully within your limits.

What is a kill switch in automated trading?

It is a preset halt on new entries that you can trigger at any time. In PitBridge it is file-backed, so a person can engage it even if the daemon is wedged, and it can only be released from the CLI. De-risking such as cancel, close, and flatten stays allowed while it is engaged, so you can always get flat.

How does a daily loss halt work?

When the day's profit and loss, realized plus unrealized, reaches the floor you configured, new entries are blocked with DAILY_LOSS_HALT. With sticky day-halt on, the account stays shut until the ET session rollover even if the number later recovers. De-risking stays allowed.

Are these the same as SEC pre-trade risk controls?

They are the same class of preset size and instrument check that broker market-access systems have long run before an order reaches an exchange. That is conceptual lineage, not a compliance claim. PitBridge is software you configure, and it makes no representation about any regulation.

What happens when a guardrail blocks an order?

The order is not sent. The agent gets a structured result with blocked set true, a typed reason_code, and a detail string, never a raised exception, so it can read the reason and explain it. The whole decision is written to the audit log.

PitBridge is in development. NinjaTrader 8 is first.

Tell us your platform and we email you when your setup is supported. Nothing else.