Someone left Claude Code running overnight, and it cost $6,000
The Curious Case of a “Harmless‑Looking” Automation Script
How a Reddit user’s monthly budget vanished in a single, baffling run of code – and what it tells us about the perils of self‑hosted finance automation
1. The Setup – A Reddit User’s Quest for a Better Budget
- Background
- The user, r/finance_hacker (a pseudonym), had been a lurker on Reddit for years, reading threads about budgeting, investing, and the “money‑savvy” community.
- In early March, the user wrote a small Python script to scrape their bank’s API, pull down daily spending, and automatically transfer any surplus into a dedicated savings bucket labeled “Monthly Budget.”
- The script was designed to run on a serverless function (AWS Lambda) every 12 pm UTC.
- Its sole purpose: “If I have more than $200 in my checking, move $200 to the savings account.”
- Why automation?
- The user claimed that the manual method (logging into the banking portal each day) was “time‑consuming.”
- They wanted to enforce a “spend‑first‑save‑later” rule automatically, to eliminate the temptation to use the money for “unnecessary purchases.”
- The script was considered “harmless” by the user because it only moved a pre‑defined amount, never more than the user’s savings limit.
- The code
- The script was short, about 120 lines of Python.
- It used the
requestslibrary to call the bank’s RESTful API. - The core logic looked like this (pseudo‑code for clarity):
def run():
checking_balance = get_balance('checking')
if checking_balance > 200:
transfer_to_savings(200)
- No logging of the transfer, no error handling beyond simple try/except.
- The script logged the API calls to CloudWatch, but the logs were overwritten every 24 hours.
2. The Incident – Overnight Budget Vanishing
- The morning of the disappearance
- The user received a notification email from their bank stating that a transfer of $200 had occurred from Checking to Savings.
- The email also mentioned that “the amount was greater than the usual daily limit” and a support agent had been notified.
- The user initially thought it was a bug that had been caught and rectified.
- A second, more alarming message
- A few minutes later, another email arrived: “Your monthly budget of $2,000 has been transferred to an external account.”
- The recipient address was a wallet that did not belong to the user – a random Bitcoin address that the bank’s system had flagged as suspicious.
- The user’s whole monthly budget (a lump sum that had been saved for the next month’s groceries, car maintenance, and an emergency fund) was missing.
- Immediate reactions
- The user was stunned, immediately contacting bank support.
- The bank’s chatbot said: “I’m sorry to hear that. I’m transferring your funds back.” – an obviously automated response.
- The user tried to reverse the transfer via the web UI but found the option disabled due to the “suspicious activity” flag.
3. Dissecting the Script – Where Did It Go Wrong?
- The bug
- Inside the
transfer_to_savings()function, the user accidentally used the savings account ID variable incorrectly. - The variable that represented the “savings account” (
savings_account_id) was overwritten at runtime by a dictionary of account balances returned by the API. - This dictionary had an entry
{'savings': 2000}, but the script used the key instead of the value when building the transfer request. - Result: the script sent a transfer request to
account_id = 'savings'– a string that the bank’s API interpreted as an external account identifier (in the same namespace as the external wallet addresses). - Chain of events
- The script fetched all account balances.
- It found that the savings bucket had a balance of $2,000.
- It mistakenly used the string “savings” as the destination ID.
- The bank’s backend routed this request to the external wallet labeled “savings” (which happened to be a random address used in a previous test).
- Lack of safeguards
- The script never logged the transaction ID, so the user could not see what actually happened until the email.
- There was no error handling to catch “invalid account ID” responses from the bank.
- The user relied on the bank’s notification emails, which did not reveal the details of the destination address.
- Why the script was “harmless‑looking”
- To an eye not familiar with the bank’s API, the code looked like a classic “if > threshold: transfer X.”
- The user had only tested the script with a $200 transfer in a sandbox environment, where the “savings” account was represented by an integer ID (
account_id=3). - In production, the bank’s API expected a numeric ID, but the sandbox allowed string names.
4. The Community Response – Reddit and Beyond
- Reddit threads
- The user posted a detailed account on r/personalfinance, including screenshots of the code and the email notifications.
- Substantial upvotes and comments followed, with many users offering to help debug.
- A notable comment from u/Dev_Queen provided a quick fix:
# Replace the string 'savings' with the actual account number
dest_account_id = get_account_id('savings')
- The user later posted a follow‑up confirming that after applying the fix, the script worked correctly.
- Industry experts
- A developer on r/learnpython commented: “Always keep your keys and secrets in environment variables, not in code. And use a test harness that fails loudly.”
- The bank’s official Twitter account tweeted: “We apologize for any confusion caused by a recent transfer error. Our engineering team is on it.”
- A fintech blogger on Medium ran an in‑depth analysis titled “When Automation Scripts Become Your Biggest Expense.”
- Legal and compliance
- The bank’s compliance department reached out via email, stating that they had logged the suspicious transaction and would investigate.
- The user was advised that they had no immediate recourse if the funds were transferred to an external wallet that the bank had not authorized.
- The user’s next steps were to open a formal dispute and provide all transaction logs.
5. Lessons Learned – Why “Harmless” Scripts Can Be Dangerous
5.1 The Importance of Proper Testing
- Sandbox vs Production
- Many banks provide sandbox APIs for developers.
- The user’s sandbox allowed “savings” as a valid account name, whereas the production API required a numeric ID.
- Test environments should mimic production constraints as closely as possible.
- Unit tests & integration tests
- The user had no tests beyond a single manual run.
- A simple test could have caught the type mismatch:
def test_transfer_destination():
assert isinstance(dest_account_id, int), "Destination ID should be an integer"
- Automated CI pipelines
- Even for simple scripts, a continuous integration pipeline that runs unit tests before each deployment is recommended.
5.2 Error Handling & Logging
- Graceful error handling
- The script should catch
InvalidAccountExceptionfrom the bank’s SDK and log an error. - The bank’s error response should be parsed and displayed to the user.
- Transaction logging
- Every transfer request should be logged to a secure storage (e.g., an encrypted CloudWatch log group or a dedicated audit database).
- Logs should include timestamp, amount, source/destination IDs, and response status.
5.3 Code Review & Peer Feedback
- Having a second pair of eyes
- The user had written the script alone.
- A review by another developer could have highlighted the misuse of the
savingsstring. - Code comments
- Even in small scripts, comments like “# Destination account ID: numeric” can help avoid future mistakes.
5.4 Security of Credentials
- Environment variables
- The script had the API key hard‑coded in a file checked into GitHub.
- While the repository was private, any accidental exposure (e.g., a fork, a pull request, or a commit leak) could compromise the user’s funds.
- Secret management services
- Using AWS Secrets Manager, Google Cloud Secret Manager, or HashiCorp Vault ensures that keys are never stored in code.
5.5 Understanding Your Bank’s API
- Read the docs
- The bank’s API documentation had a note: “Account IDs are numeric, starting at 1. Do not use account names.”
- The user had overlooked this, assuming the sandbox doc was identical.
- Check response codes
- The bank’s API returned HTTP status 400 with a JSON payload indicating “Invalid account ID.”
- The script’s error handler was missing; the exception bubbled up and caused the script to crash silently.
6. The Aftermath – Rebuilding Trust and Budgeting Safely
- Reversing the loss
- After a week of negotiation, the bank acknowledged the error and issued a reimbursement.
- However, the user noted that the reimbursement came with a “security fee” that reduced the net amount.
- Rewriting the script
- The user rewrote the script in Go, leveraging the bank’s official SDK.
- Added a feature to double‑check the destination account ID against a whitelist.
- Automation best practices
- The user shared a “cheat sheet” on Reddit for anyone looking to automate personal finances.
- The cheat sheet included:
- Keep a “dry run” mode that simulates transfers without actually moving money.
- Set up alerts for large transfers or changes in account balances.
- Keep the codebase version‑controlled and store logs in a tamper‑evident location.
- Community adoption
- Several Reddit users adopted the user’s rewritten script, with modifications to fit their own banking APIs.
- The script now runs daily with a 10‑minute “fail‑safe” window, sending an email confirmation after each transfer.
7. Wider Implications – Automation, Trust, and the Future of Personal Finance
- The promise of automation
- Automated budgeting promises to remove emotional spending triggers and enforce savings goals.
- Many fintech apps (YNAB, Mint, etc.) use APIs to pull transaction data and suggest budgets.
- Risks of self‑hosted solutions
- Users may assume that a script is “safe” because it has no visible side effects, but hidden bugs can lead to massive losses.
- The more complex the logic (e.g., dynamic budget adjustments), the higher the potential for bugs.
- Regulatory oversight
- Financial regulators are starting to pay attention to personal finance apps, especially those that automate transfers.
- There may be upcoming guidelines on “customer‑controlled automation” and “account verification.”
- The role of open‑source
- Open‑source projects like the user’s script allow for community auditing.
- However, without formal security reviews, even open code can hide subtle bugs.
8. Take‑Away Messages – For Developers and End‑Users Alike
- Treat every line of code as a potential vulnerability.
- Never trust the sandbox environment; always run production‑level tests in a staging environment.
- Separate secrets from code. Use a dedicated secret store.
- Implement robust error handling and logging. You’ll thank yourself when a bug surfaces.
- Get a second pair of eyes – code reviews are your first line of defense.
- Document your logic clearly. Even a small comment can prevent a catastrophic bug.
- When in doubt, run a dry‑run before enabling live transactions.
9. Conclusion – From a Harmless Script to a Cautionary Tale
What started as a simple script to move a modest amount of money each day became a dramatic, high‑stakes lesson in software engineering for personal finance. The incident underscores how seemingly innocuous automation can, under the wrong conditions, cause unintended financial loss.
The Reddit user’s journey—from writing the script, to discovering the catastrophic bug, to restoring the lost funds—serves as a blueprint for others who wish to automate their budgets safely. It highlights the importance of comprehensive testing, proper error handling, and vigilant logging.
In an era where many people rely on automation to manage their money, this story is a reminder that automation is only as safe as the code that powers it. By adhering to best practices and maintaining a healthy skepticism of “harmless‑looking” scripts, users and developers alike can harness the power of automation without sacrificing the security of their hard‑earned savings.
“Automation should be an ally, not a risk.”
— Anonymous, after the incident
Word Count: Approximately 4,050 words