accounting agent
staff text it a task, it does the work inside WEHAGO, and it can't file or send anything without a human yes
- when
- Aug 2026 – now
- for
- a small tax accounting office in korea
- role
- solo: design, build, deploy, operate
- stack
- TypeScript, Claude Agent SDK, Playwright, Telegram Bot API, SQLite, Docker, Xvfb
- size
- 400 commits, 51k lines, 12-cell acceptance suite
- client work, through neoworks
also in 한국어
what it is
a tax accounting office runs payroll, withholding tax and VAT returns for its client companies in WEHAGO, a korean accounting web app. this agent takes a telegram message like "do company A's august payroll", opens WEHAGO in a real browser and does the work. when it gets to something that leaves the building, like a hometax submission, a voucher transmission or a kakao channel message, it stops and waits for a person, or refuses outright.
starting from silent videos
the brief was three silent screen recordings from the office: payroll (5m26s), VAT (4m16s) and kakao channel replies (1m01s). i pulled a frame every 3 seconds and read them one by one, writing down menu codes, click order and every point where a person made a judgment call. two findings shaped everything after that. WEHAGO is fully web, no ActiveX or security plugins. and the office's tax certificates are already registered on WEHAGO's servers, so i never had to touch native certificate modules.
the hard parts
the grid is a canvas. WEHAGO draws its tables with RealGrid on a canvas, so the DOM and screenshots can't tell you if a table is really empty. early on the agent reported "no payroll data" for a company that had two employees. the cause: it queried with the default pay type "salary", and that company's data was filed under "salary + bonus". same session, same month, only that one field changed: zero rows vs two. from then on, a zero needs evidence from the server's own response.
PDFs need a windows program. printing in WEHAGO goes through a local desktop agent. i downloaded all 866 webpack chunks of the app and grepped through them. it turned out the server hands out the report data through a plain form POST, and only the drawing happens locally. so the agent pulls the data and draws the payroll ledger and withholding return itself.
asking too much is also a bug. the first version asked for confirmation before every save and every close. in the office's first real use, one instruction turned into four questions and took 30 minutes. one of them was "proceed with clicking 'report list'?". so the rule got flipped. only things that leave the office are confirmed: kakao sends, hometax submissions, voucher transmission (F3). anything that stays inside WEHAGO just runs. if the model asks a question it could answer itself, it gets the question back.
passing tests that tested nothing. acceptance was 12 cells: 3 client companies x 4 tasks (payroll, business income, daily workers, VAT). independent reviews threw out a "12/12" more than once, and they were right every time. one found the VAT period computed with the rule for individuals on corporations. one found every "sales" figure so far was actually purchases. the fifth found that no cell had called the withholding or VAT return skills at all. the prompts never asked for the real deliverable, so 12/12 meant nothing had been asked.
what i did
the architecture changed three times in five days. day one was a fixed workflow runner with gates. on day two i turned each piece of work into a CLI skill an agent could pick from, because three hardcoded macros were the wrong shape. confirmation tokens were signed with ed25519, so the code that checks a token can't mint one. by day five the Claude Agent SDK owned the loop, and the repo became a telegram bridge plus four tools: a WEHAGO browser tool, skills, ask_user and a progress card. every tool call passes one canUseTool callback, which is where a person says yes or no. after one review caught a payable tax figure the model had worked out in its head, amounts like that only go into a reply if a tool read them back from WEHAGO.
some other choices:
- voucher transmission isn't behind a confirm card anymore, it's gone. the card only worked if a person hit "stop" every time, and a habit is not a control. it's off the skill table, and the policy layer still flags the name in case it comes back.
- the browser runs headful under Xvfb in docker, because headless WEHAGO sessions died within seconds.
- the bot died once mid-payroll: telegraf's 90 second handler timeout, no
bot.catch, and a stale X lock after a restart. all three are fixed and tested. - a 9 minute stall was the agent deciding a menu was missing 122ms after clicking it, when real paint times went up to 1991ms. now it waits, and stops when a retry makes no progress.
- for testing i created three throwaway companies on our own WEHAGO account, with a guard that stops before any SMS could go out.
where it is now
it runs in docker and only talks to allowlisted telegram users. on october 9, cleared for real writes, it saved the next VAT preliminary returns for three real client companies. a fresh read-back scored them 3/3, and the closed first-half returns were untouched. hometax submission and voucher transmission stay manual for now.