Bot interface
Every bot in the library implements the same shape, even if the internals differ. This is what makes 97 bots feel like one system.
Canonical contract
The Bot<I, O> interface is parameterised by its input type I and output type O. Both must be schemas declared on the typed-schemas page.
interface Bot<I, O> {
// Identity (immutable)
botId: string; // e.g. "1.2", "risk.kill_switch"
version: string; // semver, bumped on any decision-affecting change
authority: "Read" | "Vote" | "Shape" | "Trade" | "Audit";
layer: "discovery" | "risk" | "execution" | "strategy"
| "intelligence" | "security" | "governance";
// Lifecycle
init(config: BotConfig): Promise<void>;
validateInput(input: I): ValidationResult;
decide(input: I, context: BotContext): Promise<O>;
// Output surfaces
explain(output: O): PlainEnglishReport;
emit(output: O): ReportEnvelope;
// Operational
health(): BotHealthStatus;
setMode(mode: LiveMode): void;
}
Why one interface
- Composable. Any bot can be wrapped by a meta-bot (replayer, shadow runner, fuzzer, slow-loris) that depends only on the interface.
- Reviewable. Two bots in the same class are directly comparable — same hooks, same outputs, same emitted envelope.
- Promotable. Promotion gates can run automatically against any conforming bot — they don't need bot-specific glue.
What each method must do
| Method | Must | Must not |
|---|---|---|
init | Validate the BotConfig against its schema; refuse to run on warning>hard violations. | Read config from disk; everything goes through BotConfig. |
validateInput | Reject stale, malformed, or out-of-domain input before decide is called. | Mutate the input. |
decide | Be deterministic given (input, context, config). No clock reads except via context.now(). | Submit orders directly; only Trade-authority bots may emit OrderIntents, and even those go through Risk first. |
explain | Produce a one-paragraph plain-English description suitable for a non-engineer. | Reveal model internals, secrets, or prices in a way that breaks the locked positioning rules. |
emit | Return a ReportEnvelope with a stable reason_code from the reason-code registry. | Free-text only. Free text is a logging anti-pattern. |
health | Return a BotHealthStatus within the bot's heartbeat budget. | Take an external lock. |
setMode | Honour live-mode semantics — never silently upgrade itself. | Persist the new mode anywhere except the audit log. |
BotContext (passed to every decide)
interface BotContext {
now(): number; // monotonic ms — never Date.now()
correlationId: string; // joins this decision to the originating event
parentDecisionId?: string; // for Risk bots reviewing an OrderIntent
mode: LiveMode;
killSwitchActive: boolean; // checked first in every guardrail
builderCode: string; // V2 attribution, present on every order
signal<T>(name: string): T | null; // typed read-only access to upstream signals
}
Forbidden in any implementation
- Reading process-wide globals or singletons. State goes through
context. - Submitting orders without a passing
RiskVote. EvenTradebots emit an OrderIntent and wait for the risk pipeline. - Catching all errors and returning a default. Failing open is the most dangerous pattern in trading systems. Use the documented
safe_fallbackper bot. - Hard-coding market IDs, contract addresses or URLs. Move them to BotConfig.