[ WHITEPAPER · v1.0 · AUGUST 2026 ]

Supervised Autonomy for Portfolio Agents

Why the safest agent in a trading loop is the one that cannot trade

Oddysey · oddysey.dev · independent project, unaffiliated with any brokerage

Abstract

Autonomous agents are good at attention and bad at accountability. An agent can watch a portfolio continuously and cheaply, which is genuinely useful; the same agent, given the ability to place orders, becomes a probabilistic process holding a withdrawal right, which is not. The prevailing response is to constrain that ability with guardrails — position caps, allow-lists, confirmation prompts. Guardrails restrict a capability that still exists, and the history of software security is largely a history of capabilities escaping their restrictions.

This paper describes Oddysey, a supervision layer that takes the other option: it removes the capability. Oddysey watches tokenized equities continuously, drafts portfolio moves when thresholds trip, and has no mechanism whatsoever for executing one. Approval is reserved for a human and is unavailable to agents at any permission level. Execution happens on the operator’s own rail, with credentials Oddysey never sees, and returns to the system only as a reported fact in an append-only ledger. We describe the architecture, the split between what a language model produces and what code computes, the tool surface designed around a capability that must not exist, and the limitations this design does not solve.

01The supervision gap

Two familiar products bracket the problem. At one end sits the alerting tool: it notices things and tells you, it can do no harm, and it produces work rather than removing it — you still read the chart, form the view, and size the trade. At the other end sits the autonomous trading agent: it removes the work entirely, and to do so it must hold the ability to move your money on the strength of its own reasoning.

The gap between them is where most operators actually live. The labour they want absorbed is attention and drafting — the continuous watching, and the first version of the plan. The labour they emphatically do not want absorbed is the decision. An alerting tool absorbs neither; an autonomous agent absorbs both.

The useful decomposition is not how much autonomy to grant, but which part of the work to delegate. Attention and drafting are safe to hand over because their worst output is a document. Authorisation is not, because its worst output is a position.

Guardrail-based systems accept the coupling and then try to bound it. This has two failure modes that recur regardless of implementation quality. First, the guardrail is code that can be wrong, and every bug in it is a live withdrawal. Second, and more insidious, a confirmation prompt attached to an otherwise autonomous system trains the operator to approve: after fifty confirmations that were all fine, the fifty-first is not read. An approval step is only meaningful when refusing is a live possibility, and that requires the operator to be reading an argument rather than clearing an obstacle.

02Design principles

Four invariants shape every decision in the system. They are structural properties, not configuration.

  1. P1

    No execution path exists.

    Not disabled, not permission-gated — absent. The codebase contains no endpoint, tool, or function that places an order. A proposal reaching executed did so because a fill was reported from outside.

  2. P2

    No agent can authorise.

    The tool surface exposed to agents omits approval entirely. An agent may create watches, read proposals, run an evaluation, and report an execution; it cannot decide that a draft is acceptable.

  3. P3

    Models argue; code computes.

    A language model produces the title, the reasoning, and a proposed target allocation. Every quantity an operator would act on — current allocation, portfolio value, trade notional — is computed from real quantities and real prices.

  4. P4

    Provenance is never inferred.

    Every price carries its source and a liveness flag. When any symbol falls back to a placeholder, the whole response is marked not-live, on the screen and over the protocol alike. The system degrades visibly rather than plausibly.

A useful test of such principles is what they cost. P1 cost the product its most obvious feature and an entire completed brokerage integration, which was written, tested, and then switched off. P3 costs draft yield: a model reply whose allocation invents a symbol or fails to sum to 100% is discarded rather than repaired. Principles that cost nothing to hold are not principles.

03System architecture

Oddysey runs entirely on edge infrastructure: a Cloudflare Worker serving a Next.js application, SQLite-compatible D1 for persistence, and a Cron Trigger driving the evaluation sweep. There is no long-running agent process — the “fleet” is a scheduled function, and the naming is presentational.

COMPONENTROLE
Command DeckThe operator's surface. The only place approval happens.
Quote serviceCache-first pricing with per-symbol provenance and a fixture of last resort.
Evaluation sweepA scheduled function that turns tripped watches into drafted proposals.
Drafting agentOne structured model call per tripped watch, schema-validated on return.
LedgerAppend-only record of every request, plan, decision, and reported fill.
MCP serverThe operator's own agent's window onto approved work.

Data is scoped per operator at every query. Sessions are signed cookies carrying an address, with no server-side session table to steal. The single deliberately shared table is the quote cache, on the grounds that a price for a symbol is the same fact for every operator, and sharing it is what keeps the system inside an upstream rate limit that applies per egress address rather than per account.

04The evaluation loop

The sweep is the component that makes the system do anything while nobody is watching. Its structure is unremarkable; its interesting properties are the refusals.

sweep, per operator
every 15 minutes, for each operator with active watches:

  quotes ← quote service (cache → provider → fixture, provenance kept)

  for each active watch:
      if no quote                     → no-quote     · nothing drafted
      if change24hPct > threshold      → held         · nothing drafted
      if fired < 30 min ago            → cooling      · nothing drafted
      else:
          draft ← model(watch, quote, positions)   # prose + target alloc
          validate(draft)                          # schema + allocation rules
          if invalid → draft-failed · stamp cooldown · ledger[system]
          else       → proposal(pending)
                       stamp cooldown
                       ledger[alert] + ledger[plan]

Three refusals are worth naming. A watch that fired within the last thirty minutes is skipped, because a symbol sitting below its threshold would otherwise re-draft on every tick and bury the operator in near-identical documents — and an operator buried in documents stops reading them, which converts a review queue back into a rubber stamp. The cooldown is stamped even when drafting fails, so a model outage cannot become a loop that spends money once per tick for as long as the price stays down. And drafting is sequential rather than concurrent: the loop is not latency-sensitive, while a burst of parallel model calls is expensive.

One property is a consequence rather than a decision. The trigger condition reads change since previous close — the number shown beside the position, so that what the operator sees is what fires. Underlying equities do not trade continuously, so outside market hours that number stops moving and the loop goes quiet. This is correct behaviour for the price source in use, and it is in tension with the promise of round-the-clock watching. We treat it as an open problem rather than a solved one; see §11.

05Division of labour: model and code

Language models are strong at articulation and weak at arithmetic, and a system that ignores this produces documents that read well and total incorrectly. Oddysey draws the line explicitly.

ARTEFACTPRODUCED BYFAILURE HANDLING
TitleModelLength-bounded; rejected if empty.
RationaleModelBounded 40–900 characters.
Proposed allocationModel, validated by codeRejected outright if it names an untracked symbol, repeats one, or misses 100% by more than half a point.
Current allocationCodeComputed from quantities × prices.
Portfolio valueCodeComputed.
Trade notionalCodeComputed as half the total absolute drift.

The model is handed a JSON schema and its reply is re-validated against that schema before anything is persisted: a model is never trusted to have obeyed its own contract. Critically, invalid output is discarded rather than repaired. Normalising a malformed allocation to sum correctly would produce a document that looks authored and is not — the operator would be reading a plan that no one, human or model, actually proposed.

The prose exists to be argued with. The numbers exist to be relied on. Mixing the two — letting the model state figures, or letting code write the reasoning — destroys the property that makes a drafted proposal reviewable at a glance.

06The approval boundary

Oddysey exposes a remote MCP server so an operator’s own agent can collect approved work, execute it, and report back. Nine tools: read the portfolio, list and create and pause watches, list and read proposals, read the ledger, run an evaluation, and record an execution.

The tenth tool, which does not exist, is the design. There is no approve and no reject, at any permission level, for any client. An agent capable of approving its own drafts collapses the entire system into an autonomous trader with extra steps; the supervision would be theatre. Because the capability is absent rather than gated, no misconfiguration, prompt injection, or compromised token can produce it.

the trust boundary
           ┌──────────────────────────────────────────┐
           │            CAN MOVE MONEY                │
           │                                          │
           │   operator's own agent, on the           │
           │   operator's own rail, with the          │
           │   operator's own credentials             │
           └───────────────────▲──────────────────────┘
                               │ reads approved work
                               │ reports fills back
  ─────────────────────────────┼───────────────────────── trust boundary
                               │
           ┌───────────────────┴──────────────────────┐
           │          CANNOT MOVE MONEY               │
           │                                          │
           │   Oddysey: watches · drafts · records    │
           │   no venue credentials, no order path    │
           └──────────────────────────────────────────┘

The boundary is also where custody risk was removed. A complete brokerage connection layer — OAuth with PKCE, dynamic client registration, refresh tokens encrypted under a key held outside the database — exists in the codebase and is switched off. Since the system cannot execute, holding a credential that could place real orders would purchase the highest-value secret in the architecture in exchange for no capability at all. Removing a feature is a cheaper security control than defending it.

One consequence deserves stating rather than burying: the executing agent, running on the operator’s own infrastructure with the operator’s own venue credentials, is more powerful than anything inside Oddysey. The claim here is not that agent autonomy has been made safe. It is that the supervision layer does not add to the blast radius, and that the decision to act was made by a person who read an argument.

07Accountability and the ledger

Every state transition appends an immutable event, typed by what kind of act it was: a request, an alert, a plan, an approval, a rejection, an execution, a settlement, or a system event. Nothing in the system edits or deletes one.

The typing matters more than the storage. Because the kinds distinguish acts of agents from acts of humans, the ledger answers a question that a flat activity log cannot: which of these things did a person decide? For a supervised-autonomy system that is the audit question, and a record that cannot answer it does not evidence supervision — it merely narrates activity.

The honest limit is the execution event. Oddysey observes what was proposed and what was approved, because both happened inside it. It does not observe fills; it records that a client reported one. An operator reconciling against a venue statement is checking a claim, not a proof. Verifying fills against the venue’s own record is the natural extension of this work and is not implemented.

08Trust and threat model

The system’s security posture is dominated by what it cannot do. Enumerating the credentials makes the point better than a list of controls.

COMPROMISEDATTACKER GAINSATTACKER STILL CANNOT
Session cookieOne operator's deck, including approving that operator's proposals.Place an order; reach another operator.
Deck tokenNine tools on one operator's data, including reporting a false fill.Approve anything; spend anything.
Entire databaseRead all operators' watches, proposals, and ledgers.Replay a token — only SHA-256 digests are stored — or forge a session.
Cron secretTrigger the sweep early, costing model spend.Read or modify any operator's data.

No row reaches a placed order, and this is not a property of careful access control — it is a property of there being nothing to reach. Conventional measures are present where they matter: boundary schema validation on every request, parameterised queries throughout, constant-time secret comparison, hashed credentials, and error responses that carry no internal detail. They are unremarkable, which is the intent. The security argument does not rest on them.

Model output is treated as untrusted input rather than as a trusted component’s response. This is the correct posture for any system where a language model’s output crosses into a data structure users act on, and it is why a malformed allocation is discarded rather than coerced.

09Cost and operational envelope

Supervised autonomy has an unusually favourable cost profile, because the expensive operation happens only on the rare path.

Watching is nearly free: a scheduled function reads cached quotes and compares numbers. A model call — the only meaningful per-event cost — happens exclusively when a threshold trips and the watch is out of cooldown. A well-tuned deck therefore spends nothing for days and a few cents on the day something moves. The two mechanisms that keep this true are the shared quote cache, which decouples upstream calls from deck count, and the cooldown, which bounds spend per watch to roughly two drafts an hour in the worst case.

The rate-limiting behaviour of the quote provider proved to be the sharpest operational constraint, and for an unobvious reason: providers limit by IP, and edge-runtime egress addresses are shared among unrelated tenants. A key that answers comfortably in development can be throttled in deployment. Sequential fetching with spacing, a single retry, and a cache that accumulates partial results resolves this — the system heals to live prices within a few requests rather than failing hard, and says which state it is in throughout.

10Limitations and open problems

  1. L1

    Market hours versus continuous watching.

    The trigger reads change since previous close on the underlying equity, which stops moving when the exchange is shut. Either the sweep becomes market-hours-aware and stops claiming continuous coverage, or — if tokenized instruments genuinely trade outside exchange hours — the underlying-equity quote stops being an adequate proxy and the price source must change. Both are real work; neither is done.

  2. L2

    Reported fills are unverified.

    Execution events are claims made by an agent, not observations. Reconciliation against venue records is the obvious next step.

  3. L3

    Holdings are fixtures.

    Quantities are seeded rather than read from an account, precisely because the system connects to none. Allocations are therefore arithmetic on a hypothetical portfolio at real prices. A read-only balance connection would fix this without reintroducing an order path — reading is not trading — but reintroduces credential custody, and that trade is not obviously worth making.

  4. L4

    Approval fatigue is mitigated, not solved.

    Cooldowns and threshold discipline reduce document volume. They do not guarantee that a human reads carefully, and no mechanical control can. The design can make review possible; it cannot make it happen.

  5. L5

    No automated test suite.

    The evaluation loop and the allocation arithmetic are exactly the code that most warrants tests, and they do not have them. This is a known gap, stated rather than glossed.

  6. L6

    Single-operator scope.

    There is no notion of a team, a reviewer distinct from an approver, or a four-eyes requirement. For desks, that is the first structural feature missing.

11Conclusion

The debate about agent autonomy is usually framed as a dial between capability and safety, with products positioned along it. Oddysey argues the framing is wrong: the useful question is not how much authority an agent should hold, but whether the authority needs to sit with the agent at all. Attention and drafting can be delegated completely, because a bad draft is a document. Authorisation cannot, because a bad authorisation is a position.

Splitting there produces a system whose security argument does not depend on its own correctness. There is no execution path to defend, no venue credential to protect, and no approval tool to misconfigure. What remains is a supervision layer that watches without tiring, writes a case a person can read in ten seconds, and keeps a record honest enough to say who decided what.

The safest agent in a trading loop is not the one with the best guardrails. It is the one that cannot trade.

Implementation details are in the documentation; what is built, what is not, and what will deliberately never be built is in the roadmap.