Most integrations fail cheaply. A rate limit returns 429, you back off, nobody notices. A webhook drops, you replay it.
A brokerage API is not that. It runs 9:15 to 15:30 IST against a real account with real money, and every defect has a price: a duplicate order, a position that never squared off, a signal that fired into a window nobody was watching. The daemon behind VivaTrades' intraday side does exactly this, and almost none of the interesting engineering is the strategy.
The failure modes worth designing against#
Before any of the mechanics, it is worth being precise about what can actually go wrong, because it is a short and specific list:
What costs money
Duplicate orders
Doubles exposure
A crash between placing and recording, then a restart that places again
Stuck positions
Overnight risk
A square-off that partially fills and is not followed up
Missed square-off
Intraday becomes delivery
The daemon busy or dead at 15:15
Silent failure
Nobody knows until settlement
An error swallowed by an empty catch, no alert
Notice what is not on that list: strategy quality. A bad signal loses a predictable, bounded amount. A duplicate order loses an unbounded one, and it does it while you are asleep.
Rate limits are a design input, not an error handler#
Kite's limits are roughly 10 requests per second and 3,000 per day for order-related calls. Those numbers are generous for a strategy that acts a handful of times a day and immediately hostile to a tight loop.
The important consequence is architectural rather than defensive: a daily cap means a retry storm can exhaust the budget for the session. Burning 3,000 calls by 11am does not degrade the system gracefully — it leaves open positions with no ability to place the orders that would close them. The rate limit stops being a throughput concern and becomes a liveness one.
So the review checklist for anything touching this code asks about tight loops
around placeOrder before it asks about anything else, and every
placeOrder / modifyOrder / cancelOrder has to carry explicit error
handling. A silent .catch(() => {}) is the specific pattern that turns a
rejected order into a position you believe you closed.
The clock is the hard dependency#
Three timers govern the session, and each exists because a specific thing breaks without it:
- 08:30 — token check. Kite access tokens expire daily around 08:30 IST. A daemon that discovers this at 09:15 has already missed the open. Checking early and alerting on Telegram means the failure lands 45 minutes before it matters, while a human can still act.
- 09:12 to 15:15 — scan every 3 minutes. The loop starts before the market opens rather than at the bell.
- 15:15 — square off. Intraday positions left open become a settlement problem, so this is the one deadline the system genuinely cannot miss.
The scan cadence is driven by an interval timer, and the loop carries an explicit in-flight guard: if a scan is still running when the next tick arrives, the tick logs scan already in progress and returns. Without that, a slow scan quietly becomes two concurrent scans looking at the same signals and reaching the same conclusions twice.
Alerting is part of the trading path, but must never block it#
Every meaningful event goes to Telegram: token expiry, order placement, the square-off result, and errors with the specifics attached. That last part is the difference between a useful alert and a notification — closed N open MIS positions at 15:15 can be checked; square-off completed cannot.
The rule that makes it safe: an alert failing must never stop trading. If Telegram is unreachable, the daemon carries on and the failure surfaces elsewhere. Wiring a notification channel into the critical path means an outage at a messaging provider becomes an outage in your trading system, which is a spectacular way to lose money to something irrelevant.
The heartbeat follows the same logic in the opposite direction. The daemon POSTs every two minutes, and a failure to post is deliberately silent locally — because the point of a heartbeat is that something else notices its absence. A process that alerts on its own failure to report is solving the easy half of the problem.
Restart is the question that matters#
Here is the one I would look at first in any system like this, and it is worth being honest that it is a question rather than a solved problem.
If the daemon crashes between placing an order and recording that it placed one, what happens on restart? PM2 will bring the process back. If the process reconstructs its intent from the same signals, it may place the order again.
The two standard answers are an idempotency key — Kite's tag field, set
deterministically so a repeat is recognisably the same order — or a local
order ledger written before the call, so a restart can reconcile what it
intended against what the broker actually holds.
The daemon tracks entries and exits in memory to avoid duplicate alerts, and that is a different guarantee: it dedupes what you are told, not what is placed, and it does not survive a restart. Reconstructing from the broker's own order book at startup is the version that does.
This is exactly why the review agent for this code exists and why its checklist leads with idempotency. A checklist is most valuable where the answer is not yet "yes". Encoding the question in a reviewer that runs on every change to these files is how it stays asked, rather than being noticed once and forgotten.
Square-off deserves its own paranoia#
The 15:15 deadline has failure modes the rest of the session does not:
- Partial fills. If a position is 100 and 60 fill, the remaining 40 is an overnight position nobody decided to hold. The alert has to say the remainder, not just report that a square-off ran.
- Timing collision. If the daemon is mid-scan at 15:15, the square-off needs priority over more signal evaluation. Entering a new intraday position at 15:14 is a bug with a same-day cost.
- Failure alerting. A failed square-off is the single most expensive silent failure available, so it alerts with symbol, quantity and remainder.
What generalises#
Very little of this is about trading. The transferable parts:
- Rate limits with a daily cap are a liveness concern. Budget them for the worst hour, not the average one, and remember that exhausting them can leave you unable to perform the closing action.
- Alerting belongs beside the critical path, never inside it.
- A heartbeat is only useful if something else watches for silence.
- Ask what a restart does mid-operation, and prefer a durable record written before the side effect over any amount of in-memory bookkeeping.
- Where the answer is "not yet", encode the question somewhere that runs automatically. An unanswered question in a checklist that executes beats a solved problem in someone's memory.
The general principle: when a bug costs money rather than a retry, the engineering effort moves from making the happy path good to making the interrupted path safe. Almost everything above is about the second one.