Most teams building a Xero or Sage hook treat the client's chart of accounts as something you map once during setup and never revisit. You hardcode account codes into your sync logic, ship it, and move on. Then three months later the bookkeeper adds a new expense category or renames a tax line item and your integration silently posts to the wrong account — or just stops posting. We see this pattern constantly when onboarding clients who had a previous developer wire up their accounting integration.
The fix is boring but effective: store account mappings in a table the client can edit, not in your codebase. Build a small admin screen that pulls the current chart of accounts from the API, lets someone pick which account maps to which transaction type, and flags unmapped categories before they hit the ledger. It adds maybe a day of work to the initial build. It saves you from emergency debugging every time the finance team tweaks their structure, which they will, because that is literally their job.
Treat the chart of accounts the way you would treat user roles — as data that changes at the client's pace, not yours. If you are syncing invoices, payments, or journal entries, add a validation step that checks mappings are still valid before each batch posts. Log the failures somewhere the client can see them, not just in your server logs. The goal is that when their bookkeeper moves a line item on a Tuesday morning, nobody needs to phone a developer by Tuesday afternoon.
GPT-4o is impressive until you ask it to count the rows in your CSV correctly
We had a client last month who wanted to use ChatGPT to summarise monthly sales exports — around 1 200 rows of transaction data pasted straight into the chat window. The summaries read beautifully. They were also wrong. Totals were off by five to fifteen percent, category breakdowns quietly dropped rows, and when asked to count entries matching a filter, the model confidently returned a number that didn't survive a COUNTIF check. This wasn't a hallucination in the traditional sense; it was an arithmetic one, and it's far harder to catch because the output looks structured and plausible.
The root issue is that large language models process tabular data as token sequences, not as actual grids with indexable cells. Once your dataset exceeds a few hundred rows, the model starts losing track of position. It may silently truncate, double-count, or round in ways that no spreadsheet function ever would. Adding "be precise" to your prompt doesn't fix a context-window limitation. Even Code Interpreter mode, which spins up a Python sandbox, can stumble if the file has encoding quirks or mixed delimiters — and the model rarely flags that it failed to parse every row.
If you're using an LLM to work with structured data, treat it as a drafting tool, not a calculation engine. Run the numbers in a proper environment — a script, a notebook, even a pivot table — and let the model handle the narrative layer on top of verified figures. One useful pattern: compute your aggregates first, pass them in as context, and prompt the model to write the summary from those anchored numbers. You get the fluency without the silent errors.
Your staging environment drifts from production on day two and nobody notices until launch night
Most small teams spin up a staging server once, copy the production config, and assume parity. Within a week the two environments diverge. Someone bumps a Node version on staging to test a fix. A new environment variable gets added to production but not to the .env.example file. The database gets a migration on staging that was later rewritten before it hit prod. None of this shows up until you deploy on a Friday evening and everything falls over in ways you cannot reproduce locally.
The fix is not a bigger budget or a fancy orchestration platform. It is a single infrastructure-as-code file — a Docker Compose definition, a Terraform module, even a well-maintained shell script — that both environments are built from. The key discipline is that nobody manually tweaks either environment without updating that shared definition first. We enforce this with a simple rule: if you SSH into staging to change something, you open a PR to the infra repo before you log out. No exceptions.
This also means running the same database engine version, the same queue driver, and the same object storage provider in both places. Swapping S3 for local disk in staging because it is cheaper will hide an entire class of bugs around signed URLs, CORS headers, and upload size limits. Keep staging small — a single low-spec VM is fine — but keep it honest. The ten minutes you spend maintaining parity saves the three-hour incident at midnight.
Your payment fallback isn't a fallback if it requires the same uptime as your primary
Most South African e-commerce builds we audit have a single payment gateway — usually Payfast or Yoco — and a vague plan to 'switch to the other one if it goes down.' That isn't a fallback. Integrating a second gateway after your primary is already failing means deploying untested code under pressure, probably during a traffic spike, possibly during load-shedding when your own infrastructure is limping. A real fallback is pre-integrated, tested monthly, and can activate without a deploy.
The pattern we use is straightforward: abstract your payment calls behind a thin service layer that holds credentials and route logic for two gateways. The secondary doesn't need feature parity — it just needs to handle your core purchase flow. We keep Payfast as primary and a simple Yoco card integration as secondary, or vice versa. The switch is a feature flag, not a pull request. When gateway A returns three consecutive 5xx responses, the flag flips automatically and alerts the team. We've had this trigger twice for clients in the last eighteen months, both times on Friday afternoons.
The objection is always cost: maintaining two integrations sounds like double the work. In practice the abstraction layer takes a day or two to set up, and the second gateway integration is simpler because you've already modelled your payment domain properly. The monthly test is a single synthetic transaction in sandbox mode. Compare that to the revenue lost during a two-hour outage on payday weekend in South Africa, when your customers will simply buy from whoever's checkout actually loads.
That testimonial slider on your landing page is hiding your best social proof behind a five-second timer
Most landing pages we audit have the same pattern: three to five client testimonials rotating in a carousel with auto-advance. The designer liked how it kept the section compact. The problem is that visitors spend an average of ten to fifteen seconds scanning above the fold before deciding to scroll or bounce, and your strongest quote is sitting at position three, waiting its turn. Auto-rotating carousels have a well-documented interaction problem — users either miss the content entirely or feel interrupted when the slide changes mid-read. You are actively suppressing your own trust signals.
When we rebuilt a Cape Town fintech client's pricing page last month, we replaced their three-slide testimonial carousel with two static quotes placed inline, one directly beneath the pricing table and one beside the call-to-action button. Each quote was attributed with a real name, company, and a small headshot — no logos-only strip, no anonymous "happy customer" filler. The page's form completion rate climbed noticeably within the first two weeks, with no other changes to copy, layout, or traffic source.
The fix is almost embarrassingly simple. Kill the carousel. Pick your two strongest testimonials — ideally ones that address a specific objection your buyer has at that point on the page — and place them statically where they do the most work. A quote about onboarding speed belongs near the sign-up form, not buried in a rotating widget three screens up. Trust signals only convert when people actually read them.
Your code review comments vanish the moment the pull request merges
Every pull request you review generates small decisions — why you chose one approach over another, what edge case you spotted, which trade-off you accepted. Those conversations live in your Git hosting platform's PR thread, and the moment that branch merges, nobody ever reads them again. Six months later a developer touches the same module, makes the same mistake, and a reviewer catches it again with the same comment. The knowledge existed. It just wasn't anywhere useful.
Some teams try to fix this by writing longer commit messages or maintaining architecture decision records. Both help, but they miss the granular stuff: the 'don't use parseFloat here because Rand values with trailing zeros get silently truncated' kind of knowledge that only surfaces in review. We started keeping a lightweight review log — a single markdown file per project, updated when a PR review surfaces something non-obvious. It takes thirty seconds to add a line. The file lives in the repo itself, so it's versioned and searchable alongside the code.
The trick is being ruthless about what goes in. Not every nitpick deserves a line. You want the things that would bite a new team member or a future-you who's forgotten the context. After three months our log had about forty entries across two active projects, and we'd already linked back to it in four separate reviews instead of re-explaining something from scratch. If your review culture is generating genuine insight, give that insight somewhere to live beyond a closed PR thread.
Kill the feature before you kill the timeline by making your MVP a miniature version of the final product
Most founders we work with arrive with an MVP spec that is really just the full product with fewer animations. They've cut polish but kept every workflow. The result is a three-month build that validates nothing specific, because when it underperforms you can't tell whether the core idea failed or whether users bounced because the settings page was half-baked. A useful MVP isn't a scaled-down replica. It's a deliberately lopsided product that does one thing well enough to provoke a decision from a real user: pay, sign up, or walk away.
The practical move is to pick the single riskiest assumption in your business model and build only what tests that assumption. If you're betting that landlords will self-list furnished apartments for short stays, your MVP is a listing flow and a search page — not a booking engine, not a chat system, not an admin dashboard. You hardcode everything else, run it on a spreadsheet, or do it manually behind the scenes. The point is to learn whether landlords will actually list, not whether your notification system scales.
This feels uncomfortable because it means launching something visibly incomplete. But incomplete and focused beats complete and vague every time. When a client pushes back, we ask one question: if this feature didn't exist and users still converted, would you build it at all? That usually trims thirty percent of the scope in a single conversation. Decide what you're testing before you decide what you're building, and let that single question shape every line of your spec.