stardust

Field notes from a live, autonomous trading agent

Stardust - an experiment in agentic stock trading

Stardust is an autonomous AI agent that trades the Indian stock market โ€” premarket scan, intraday management, end-of-day reconciliation, on a schedule it partly writes itself. This is a walkthrough of how it's built, for anyone assembling something similar.

๐Ÿ–ฅ๏ธRuns on a self-hosted Hermes agent gateway, on a plain Ubuntu VPS โ€” no managed platform underneath it.
โฐWakes itself on a cron schedule it partly rewrites โ€” seven jobs a day, four where the model reasons, three that are just scripts.
๐Ÿ“š77 skills sit in an index; only the one a job needs gets loaded into context โ€” the rest cost nothing until called.
๐Ÿ”Every production mistake gets written back into its own instructions โ€” the same bug can't happen the same way twice.
ยง Origins

This started as a bet, not a business.

Why an autonomous agent is trading real rupees, and why the whole design is public.

I saw nof1.ai before I saw a reason to build this โ€” a leaderboard pitting different models against each other, each handed real money to trade crypto. My friend Manan sent it over, and the question stuck: can these models actually generate money, or does agentic trading fall apart the moment fees, slippage, and real order books get involved? We turned it into a friendly competition โ€” who could build the better system for the Indian market, not crypto.

Getting there took three attempts at the framework, not the strategy. I ran versions on OpenClaw, on Pi, and on others, largely to learn agentic systems thinking by building one badly first. Pi came closest โ€” a minimal, highly extensible core with real self-learning loops โ€” but it's CLI-first: no clean way to run a web dashboard or a Telegram bridge, and it fumbled restarting into a fresh daily context. A nudge from another friend, Nikhil Jois, pointed me at Hermes. I scrapped the Pi build entirely and rebuilt from scratch on Hermes โ€” better stability, better memory, better self-learning, full stop.

Stardust went live on Dhan on 18 August 2026. My existing portfolio stayed ring-fenced at first while the agent found its feet โ€” through a static-IP outage, a timezone bug, and a security scare, in roughly that order. Once it held up, I opened the whole account to it on 1 September: โ‚น30,000 of real capital to start, โ‚น90,000 under management as of this writing. None of it is fake โ€” including the roughly $0.70โ€“$1.00 a day in model tokens and the โ‚น500/month Dhan API subscription it costs just to keep the lights on.

I'm publishing the design, not selling a system, because I'd rather have people find the holes in it than find them myself, eventually, the expensive way. If this pushes even a few more people to experiment seriously with agentic trading โ€” and send real scrutiny back my way, or better, real improvements โ€” the writeup did its job. It's still a heavy lift: constant configuration, constant testing, real money at stake on both sides of the ledger. Nobody should copy this and walk away.

ยง Architecture

The model reasons. The code still decides.

Stardust splits cleanly into a trading engine that knows how to talk to a broker, and an agent layer that knows how to decide. The two only ever meet through one narrow bridge.

flowchart โ€” one request, one path, no shortcuts
flowchart LR
    A["Cron trigger
7 scheduled jobs"] --> B["Hermes Gateway
loads a skill into context"] B --> C["LLM reasons
DeepSeek v4 Flash"] C --> D["stardust plugin
33 registered tools"] D --> E["stardust_cli.py
single entrypoint"] E --> F["Trading engine
scan ยท size ยท order ยท ledger"] F --> G[("Dhan API
NSE")]

The LLM never talks to the broker directly, and never touches Python it wasn't handed. It reasons in language, then calls a named tool; the tool shells out to the same tested CLI a human would run by hand. That's a deliberate design choice, stated plainly in the agent's own operating manual: the agent figures out what to do and calls the tools, instead of running raw scripts. Every action โ€” a scan, a buy, an exit, a token refresh โ€” passes through the identical, already-hardened code path regardless of who or what initiated it.

It's also why there's no MCP server here. Dhan's API is already just Python functions the engine calls; wrapping that a second time behind a protocol server would mean two credential surfaces, two places for IPv4/whitelist quirks to diverge, two things to keep in sync. Instead the plugin registers native tools that shell into the same venv, the same scrip-guard, the same state file the CLI has always used. One environment, reused, not re-implemented.

There's a strict division of labor underneath that: a skill teaches the model how to think โ€” the mandate, the invariants, the accumulated pitfalls โ€” and a tool is the only thing it's allowed to act with. Every unit of real work โ€” a quote, a scan, an order, a ledger write โ€” exists as a named tool, never as prose telling the model to run a script from memory. The skill can reason all it wants; it still has to reach for a tool to actually do anything.

The premarket stage doesn't only look at price. It reads the day's news through the same scan pipeline and folds that straight into both the sized "setup of the day" and the standing watchlist โ€” a name can get added or dropped purely off an overnight headline, before a single candle of the new session has printed.

ยง Wake schedule

A schedule that edits itself.

Four jobs load the skill and let the LLM reason. Three are plain scripts โ€” no LLM, no token cost, nothing to reason about. Times are IST, the only timezone that matters here.

timeline โ€” the shape of one trading day
timeline
    title Stardust's day, IST
    00:00 : Midnight session, context rollover
    08:30 : Token mint, script only
    08:45 : Premarket scan, sets the trade of the day
    09:00 : Market opens, autopilot begins, cadence-synced every 5m
    15:30 : Market closes
    15:45 : End of day, reconcile against the broker
    Sunday 06:00 : Weekly scrip database refresh
JobScheduleModePurpose
STARDUST‑MidnightSession0 0 * * *agentContext rollover โ€” a clean session for the new trading day.
STARDUST‑TokenMint30 8 * * *scriptSelf-healing Dhan token mint via PIN+TOTP before the open.
STARDUST‑Premarket45 8 * * *agent462-name scan + news โ†’ one sized "trade setup of the day."
STARDUST‑IntradayAutopilotdynamic, 9:00–15:30 Mon–FriagentThe only stage that actually places or manages orders.
STARDUST‑CadenceSync*/5 9-15 * * 1-5scriptRecomputes activity tier, rewrites the Autopilot's own schedule.
STARDUST‑EndOfDay45 15 * * *agentReconcile against the broker, honest P&L, write down what was learned.
STARDUST‑ScripDBRefresh0 6 * * 0scriptWeekly rebuild of the local symbol master from Dhan's CSV.

The cadence controller

The interesting piece isn't any single job โ€” it's that CadenceSync exists at all. Every five minutes during market hours, a cheap deterministic script (no LLM call) looks at recent order activity, not position count, and decides how often the intraday agent should even bother waking up. A carried position sitting behind a live broker-side stop doesn't need a fast poll; a name that just got bought or sold does.

state machine โ€” five tiers, deterministic, no LLM in the loop
stateDiagram-v2
    [*] --> STANDBY
    STANDBY --> WATCH: a setup appears
    WATCH --> ARMED: setup goes live
    ARMED --> HOT: order fills
    HOT --> MANAGE: 2h since last order
    MANAGE --> HOT: new order activity
    MANAGE --> STANDBY: position closed, flat
    ARMED --> STANDBY: setup withdrawn
    WATCH --> STANDBY: setup expires
    HOT --> STANDBY: exits, flat, nothing armed
HOT
10m
order opened/closed in the last 2h
MANAGE
30m
holding, no fresh order activity
ARMED
15m
flat, live setup armed and waiting
WATCH
30m
flat, a setup exists but isn't armed
STANDBY
60m
flat, nothing armed, nothing to do

It only rewrites the cron schedule when the tier actually changes โ€” no churn on every tick โ€” never drives on weekends or holidays, logs every decision to cadence.json for audit, and only sends a ping when the tier flips. Expensive reasoning gated behind cheap, deterministic pre-checks.

ยง Agent prompt

Every bug becomes an instruction.

There's no clever system prompt here. The agent's real instructions are a skill โ€” a markdown playbook loaded fresh into context every time a cron job fires โ€” and it grows every time something breaks in production.

The base system prompt (SOUL.md) is deliberately generic โ€” a short, stock identity: "you are Hermes Agent, helpful, direct." It says nothing about trading. Every actual guiding principle lives one level up, in the skill that gets loaded fresh per cron job โ€” which is also why those principles can evolve daily without ever touching the agent's core identity.

Structurally, SKILL.md opens with a mandate ("maximize long-term profit, compound โ€” capital is always pulled live from the broker, never hardcoded"), a tool reference table so the model knows what its hands are before it reasons about what to do with them, and a set of hard invariants that hold regardless of what the LLM concludes in any given session.

The most transferable part, though, is the long pitfalls section โ€” real production mistakes, each rewritten generally enough to prevent the whole class of bug, not just the one instance that happened. A representative excerpt of the kind of thing that lives there:

01pitfall: positions() and holdings() are different endpoints โ€” a delivery position from a prior day won't show up in positions().
02pitfall: a TRANSIT order status is not a fill โ€” poll for an actual filled quantity before recording a position.
03pitfall: don't book an exit at the entry price โ€” that's not an exit, that's a no-op with fees attached.

Institutional memory, encoded directly into the thing that gets re-read every single session โ€” so a restart doesn't mean re-learning the same lesson twice.

ยง Tool layer

Thirty-four tools. One venv. No MCP.

Every tool is a thin JSON-in, JSON-out wrapper around the same CLI. The categories map directly onto the phases of a trading day.

Market data

quotehistoricalscannewssymbolscrip_search

Strategy

develop_strategybuild_setup

State & accounting

fundspositionsstate_getstate_setledgerpnlaumbenchmarkreconcile

Execution

buyexit

Watchlist

watchlist_addwatchlist_remove

Monitoring & reporting

monitortrades_eodopenrouter_cost

Account lifecycle

token_generatetoken_statustoken_consenttoken_consumerenew_tokenip_statusip_whitelistprofile

Pre / post-flight

preflightpostflight

Two are worth calling out specifically: preflight is the go/no-go gate a session runs before doing anything โ€” token valid, funds visible, market actually open, symbol database not stale โ€” and reconcile is read-only by design, never places a trade, and runs automatically at every stage purely to catch drift between the agent's memory and the broker's.

This is also where the token economics come from. The profile actually carries 77 skills, but a given cron job loads exactly one โ€” the other 76 sit in an index the model can name if it ever needs them, not text it re-reads every session. Pair that with 34 tools called by name instead of described inline, and the context behind any single wake-up stays close to the minimum that job actually needs โ€” cheaper per run, and faster to first token.

ยง Risk & safety

Rules the model doesn't get a vote on.

Every one of these exists because of a specific historical failure. Together they bound how much damage a confused session can do, no matter how it argues for an exception.

On scope, deliberately: Stardust currently trades cash equity only โ€” no F&O, no global markets. That's not a technical ceiling, it's a sequencing choice: prove the tools and the strategy hold up in the simplest, most reversible instrument before handing the agent anything with leverage or a different risk shape. Access widens in stages, and only after the stage before it has actually been trusted with real capital for a while.

Worth saying plainly, too: this is not a high-frequency system. The fastest wake interval anywhere in the schedule is 10 minutes, gated behind a cadence controller that actively tries to slow things down, not speed them up. Nothing here is competing on latency โ€” it's competing on not doing anything stupid between wake-ups.

Execution

Broker-side stop-loss

Every live entry immediately places a real exchange-side stop-limit sell order โ€” protection exists between wake intervals, not just when the agent happens to notice a breach.

Execution

Confirmed-fill discipline

A position is recorded only after polling the broker for an actual filled quantity, never on "order submitted" โ€” submission can still be rejected downstream.

Sizing

Hard caps, live buying power

Position and risk caps are computed against buying power pulled fresh from the broker every time, never a stale constant.

Behavioral

Churn guards

No same-day re-entry into a name just stopped out; no sell-and-immediately-rebuy to "reset" an entry โ€” flagged explicitly as a net-negative move.

Behavioral

Rate-based profit harvest

A winner is judged against a monthly-rate hurdle over its actual holding period, not a flat gain โ€” so a 12% move in a day and a 12% move in a year aren't treated the same.

Boundary

Tracked-only + adoption

An untracked broker holding can't be silently touched โ€” it has to be deliberately brought under management first.

Max concurrent setups
3
Max per position
30% of BP
Max daily risk
5% of BP
Stop-loss band
3–4%
Opportunity-cost hurdle
12% / mo
Fast-move flag
20%
Rollover trigger
3%
Run-rate trigger
1.5%

BP = live buying power at the broker, not a fixed capital figure. These are the actual constants the engine runs with today โ€” published as an example of a real, working risk config, not a recommendation for any specific account size.

ยง Data & reconciliation

The broker is right. The agent is not.

Reconciliation isn't a nightly cleanup job, it's a rule enforced at every stage: reconcile compares state against the broker's positions, holdings, funds, and ledger, and never trades โ€” it only flags drift. A recurring class of early bugs came from treating /positions (today's fills only) and /holdings (delivery carried from prior days) as interchangeable; they aren't, and the engine now asks each endpoint only what it actually knows.

Cost accounting is kept separate from trading performance on purpose โ€” LLM inference spend and broker fees are tracked against a GROSS/NET line, not folded into strategy P&L, so a good trading day and a good token-cost day are never confused for each other. Market data for the scanner leans on free sources (NSE bhavcopy, a news scraper) rather than the paid Dhan data feed, keeping the technical-analysis pipeline independent of the flakier paid endpoint.

ยง Known issues

The day the scanner blocked its own rulebook.

Three of the agent-driven cron jobs started failing intermittently with Blocked: prompt matches threat pattern 'deception_hide' โ€” Hermes's own prompt-injection scanner refusing to run the assembled prompt before the model ever saw it.

01The scanner regex-matches assembled prompts against patterns like do not tell the user โ€” a real, sensible signature for hidden instructions.
02A completely legitimate line in SKILL.md โ€” "don't state a P&L figure to the user without verifying it against Dhan first" โ€” happened to contain that exact phrase. Not an attack, just trading-ops vocabulary colliding with a security regex.
03Fixed by rephrasing the instruction โ€” same rule, different words.
04The documentation written to explain the bug then quoted the trigger phrase as an example โ€” and re-tripped the exact same scanner on the very sentence describing the false positive.

Not a security incident โ€” a clean example of a false positive from domain vocabulary, and the meta-trap of documenting a regex trigger using the literal trigger text.

ยง Performance

Ahead of the index, and the lead is shrinking.

One month of live results against NIFTY 50 and SENSEX, posted in full โ€” including the part that's getting worse.

Stardust, since 18 Aug
+11.6%
cumulative, live capital
NIFTY 50
โˆ’3.35%
same window
SENSEX
โˆ’3.81%
same window
Outperformance
~+15pp
vs both benchmarks

Week on week

WeekStardustNIFTY 50SENSEX
W2 ยท 24โ€“28 Aug+3.2%+0.09%+0.04%
W3 ยท 31 Augโ€“4 Sepre-base1โˆ’1.15%โˆ’0.97%
W4 ยท 7โ€“11 Sep+0.79%โˆ’2.09%โˆ’2.27%
W5 ยท 14โ€“18 Sep+0.62%โˆ’0.22%โˆ’0.65%
Cumulative+11.6%โˆ’3.35%โˆ’3.81%

1 W3 isn't a comparable week โ€” that's when the full account was handed over, so the capital base changed mid-week and a weekly return figure would be meaningless. It's left blank rather than massaged into something quotable.

Outperformance, each tradable week

Weekvs NIFTYvs SENSEX
W2+3.1pp+3.2pp
W4+2.9pp+3.1pp
W5+0.8pp+1.3pp
Cumulative+15pp+15.4pp

The honest read: Stardust beat both benchmarks in every clean week, and did it by going up while the market went down โ€” not by losing less. Over the month that compounds to roughly fifteen points of outperformance.

But the trend inside that number matters more than the number. The weekly edge has gone +3.1pp โ†’ +2.9pp โ†’ +0.8pp. In the most recent week the market was essentially flat at โˆ’0.22% and Stardust's +0.62% barely cleared it. Whatever the agent was exploiting in late August, it is exploiting less of it now. One month is far too short a window to call that either decay or noise โ€” which is exactly why it's published rather than waited out.

This is here for transparency, not persuasion. It is a record of what one experimental system did with one small account over one month โ€” not a track record, not a pitch, and not an offer of anything. Nobody is being solicited, no capital is being managed for anyone else, and there is no product behind this page.

A month of results on a five-figure account is statistically close to meaningless. A single bad week could erase the entire lead. The numbers are posted because publishing only the good stretches is how people end up fooling themselves first and everyone else second.

ยง Takeaways

What's actually worth stealing.