Automating MeroShare IPO Applications: A Telegram Bot on Cloudflare Workers
Every time a new IPO opens in Nepal, the same ritual plays out: log into MeroShare, hope the site isn’t crawling under load, fill in the form, type the transaction PIN, hit submit, and pray the request doesn’t time out at 3:59 PM. Multiply that by family members’ accounts and it’s a genuine chore.
So I automated it. The end state: a Cloudflare Worker that checks for new ordinary-share IPOs twice a day, applies automatically with my registered accounts, and reports everything to me through a Telegram bot. Total monthly cost: zero. Total infrastructure to maintain: none.
This post is the full story of how it works and everything that went wrong on the way — because the failures were the interesting part.
The Architecture
Telegram app ──webhook──> Cloudflare Worker ──REST──> MeroShare (CDSC) API
│ (webbackend.cdsc.com.np)
├──> KV (encrypted accounts, applied-markers, settings)
└──> Cron triggers (Sun–Thu, 10 AM & 2 PM NST)
Three moving pieces:
- The Worker (
src/index.ts) — receives Telegram webhook POSTs, routes commands, runs on cron twice a day. - The MeroShare client (
src/meroshare/client.ts) — reverse-engineered REST client for CDSC’s unofficial API. - The KV store — holds accounts with passwords and PINs AES-256-GCM encrypted via Web Crypto, plus “already applied” markers so the cron never double-applies.
Why Cloudflare Workers and not a VPS or a Raspberry Pi? No server to keep alive, generous free tier (100k requests/day), and cron triggers built in. For something that “should just work for months without me touching it,” serverless is the right shape.
Step 1: The MeroShare API (or: how to be your own API docs)
MeroShare has no public API, but the web app at meroshare.cdsc.com.np is just an Angular SPA talking to webbackend.cdsc.com.np/api/meroShare/*. Open DevTools once and you can map the important endpoints:
| Purpose | Endpoint |
|---|---|
Login (returns JWT in the Authorization header) |
POST /auth/ with {clientId, username, password} |
| Profile (BOID, demat, customerId) | GET /ownDetail/ |
| Linked bank accounts | GET /bank/ |
| List applicable issues | POST /companyShare/applicableIssue/ |
| Apply for a share | POST /applicantForm/share/apply |
| Application reports | POST /applicantForm/report/ |
The DP (clientId) is the numeric ID of your depository participant — findable via GET /capital/, an unauthenticated endpoint. There are 133 of them, IDs ranging 128–2191, which becomes relevant later.
Step 2: The Telegram bot layer
The Worker exposes exactly one webhook route. Telegram POSTs updates to it; the Worker verifies a X-Telegram-Bot-Api-Secret-Token header (set when registering the webhook), then dispatches commands:
/addaccount main,190,myuser,mypass,CRN1234,9999,10
/check → list open IPOs
/applyall 796 → apply with every registered account
/status → allotment results
/autoapply on → let the cron do it
Credentials are encrypted before hitting KV — the plaintext never touches storage, and the encryption key lives only in a Worker secret.
Step 3: The deployment gauntlet
This is where it got fun. Four separate walls:
Wall 1: Chrome wouldn’t let me in
I wanted to automate the BotFather setup through the browser. Chrome v136+ blocks remote debugging on the default profile entirely, and macOS Accessibility (AppleScript) couldn’t get Chromium to expose its AX tree — Chromium only builds it when an assistive client asks, and nothing I tried flipped the switch.
Workaround: an embedded Chromium panel with its own persistent profile. One QR scan from my phone, and the session stuck — then the bot creation was pure DOM automation: /newbot → name → username → regex the token out of BotFather’s reply. Chat ID came from sending the bot a message and reading getUpdates.
Wall 2: Telegram couldn’t resolve my workers.dev subdomain
Registering the webhook kept failing with Failed to resolve host: Temporary failure in name resolution — while dig showed the record live globally. Control test: setWebhook to example.com → fine. Another random workers.dev subdomain → fine. Mine → DNS failure.
Telegram’s resolver had cached a negative DNS answer for my subdomain (created before first deploy). A retry loop eventually won: 6 attempts, ~2 minutes, then "Webhook was set". If this happens to you: don’t debug your DNS, just retry with backoff.
Wall 3: CDSC returns 500 to Cloudflare IPs (sometimes)
The login endpoint worked from my laptop but returned 500 Operation Failed from the Worker. Not consistent — the second attempt succeeded. So CDSC’s F5 load balancer intermittently throttles datacenter IPs. The fix is boring and effective: retry once after a short delay on 5xx, and keep request rates low (800ms between accounts).
Wall 4: The silent API migration (the sneaky one)
/check failed with Failed to fetch applicable issues: Internal Server Error — but only from the Worker. Testing from my laptop with a fresh token… also 500. So: not an IP issue, an API contract change.
I pulled the current Angular bundle (main.*.bundle.js) and grepped it. The app now sends:
{
"filterFieldParams": [],
"page": 1,
"size": 20,
"searchRoleViewConstants": "VIEW_APPLICABLE_SHARE",
"filterDateParams": []
}
The old body I was sending ({"filterCompanyStocks":[],"filterStatus":["OPEN"]}) — the format in every open-source MeroShare script on GitHub — now returns a hard 500. No deprecation notice, of course. Same story for the reports endpoint (VIEW_APPLICANT_FORM_COMPLETE).
Two side quests while diagnosing this:
- F5 bot-defense cookies: replaying requests with the login session’s cookies triggered
Request RejectedWAF pages (support IDs and all). Fresh session, no stale cookies: clean pass. The lesson: when testing an API with anti-bot layers, change one variable at a time, or you’ll misread rate-limiting as header requirements. - A lurking payload bug: MeroShare’s
ownDetailreturnsboid: "02098152"(8 digits!) while the real 16-digit demat is1301090002098152. The old code usedboidfor both payload fields — guaranteed failure on a real apply. Nowdematis stored separately, and the apply payload sendsboidanddematcorrectly.
Step 4: Verify everything end-to-end
After each fix: deploy, send the command through Telegram’s real webhook, and read the bot’s reply in the chat:
/start→ command menu ✅/addaccount …→ credential verification against MeroShare, encrypted storage ✅/check→ open issues list from the new API ✅ (the fundNICEOFwas open, close date Sep 29)/accounts→ registered account with masked details ✅
The cron now runs Sun–Thu at 10:00 AM and 2:00 PM NST, filters for ordinary-share IPOs, checks KV for who hasn’t applied, and either auto-applies or pings me with a one-tap /applyall command.
What I’d do differently
- Watch the SPA bundle, not the API. A reverse-engineered API has no stability contract; the frontend is the documentation. A tiny scheduled check that diffs the bundle hash would have caught the migration earlier.
- Retry with jitter from day one. Both CDSC 5xx flakiness and Telegram’s DNS caching resolved on retry. Every external call in the bot now deserves a retry wrapper.
- Test payloads against the live API before building around them. I burned an hour on the old request shape because it looked plausible and the failure (500) looked like infrastructure.
The result
A fully serverless IPO bot: Telegram-controlled, zero-cost, auto-applying across accounts, with allotment tracking via /status. The interesting bugs — a stale negative DNS cache at Telegram, an undocumented API migration at CDSC, and an F5 WAF with a taste for malformed cookie jars — were all invisible until something end-to-end failed. Which is the recurring lesson: test the whole path, not just the pieces.
