kr0w15 min read
Financial Systems

Ankor

Order-to-cash reconciliation and revenue recognition for commerce. Lessons from building a team, raising capital, and learning what actually matters.

FintechStripeIFRS 15Reconciliation

"There's only two ways to make money in business: bundling and unbundling." — Jim Barksdale, 1995


At Juni, I watched e-commerce brands fail to get credit, not because they weren't profitable, but because they couldn't prove it. Cash flow was real. Data was scattered across Stripe, Shopify, Klarna, and a spreadsheet someone built two years ago and was now afraid to touch.

The pain wasn't analytical. It was operational. Finance operators knew exactly what needed doing. They just couldn't do it. Month-end meant downloading payouts, cross-referencing orders, hunting mismatches. The work was known. The tooling didn't exist.


The wrong first version

We built what fintech usually builds: visibility. Dashboards. Better views of data.

Users looked at it, nodded, and went back to their spreadsheets.

They didn't want to see the reconciliation. They wanted it done. The job was "make this task not exist anymore."


Accelerators, not flexibility

The rebuild: a connector layer between commerce systems (Shopify, Centra) and accounting systems (QuickBooks, Xero). Normalize the data (every payment provider describes transactions differently), then feed it into Accelerators.

Accelerators are narrow, workflow-specific agents. Not a rules engine users configure. Not a flexible reconciliation tool. Hardcoded, auditable, deterministic. Does one thing: Payment Reconciliation, or 3-Way Match, or Payout Traceability.

The key decision was not building flexibility. Enterprise tools like Duco let you configure anything. Wrong abstraction for a 3-person finance team. They don't want to build logic. They want outcomes.


What matching actually takes

The first matcher was a join: order reference equals transaction reference, amounts equal, done. Real settlement files broke it within weeks.

The failure that taught us most: a customer refunded the same amount twice, weeks apart. "Match the first available transaction" paired the July refund with the August payment. Both sides looked reconciled. Both were wrong, and nobody finds that until an auditor does.

What survived contact with real data was a two-pass engine. Candidates pair on order reference, currency, and type (refund to refund, never refund to sale). Each side ranks its candidates by timestamp proximity, not chronology, and a pair survives only when each ranks the other first. That one property does a lot of quiet work: a transaction can't be consumed twice, and split captures land on the right halves. The duplicate-refund trap just disappears. A second pass re-ranks the leftovers with amount difference first and the tolerance relaxed.

The tolerance, for the record, is one cent.

FIG 01The matching enginemutual best match, two passes
ORDERSShopify · CentraSETTLEMENT LINESKlarna · Adyen · PayPal · StripeCANONICAL SHAPESgross = net + tax + … to the centPASS 1 · MUTUAL BESTref + currency + type, |Δ| < 0.01ranked by time proximityLEFTOVERSPASS 2 · CLOSEST AMOUNTtolerance relaxed, Δ ranks firstmatchpartial_captureneeds_reviewno_match → receivableunpaired money → reviewstatuses are a work queue, not an error log
Both sides rank their candidates by timestamp proximity, not order. A pair survives only if each ranks the other first, so a transaction is consumed at most once. The leftovers get a second pass with the amount tolerance relaxed.

Everything that fails to match becomes a status: no match, needs review, partial capture. The taxonomy mattered less than what we did with it. Statuses became a work queue for a finance operator, not an error log for an engineer. An unmatched order is a receivable someone needs to chase; unpaired money is a question someone needs to answer. The product is the queue.


The money math nobody documents

Every provider describes money differently, and the differences never appear in API docs as accounting semantics. A sample from the scar-tissue list:

  • Klarna sends amounts in cents, and its gross includes VAT. Kustom (the same product, rebranded) doesn't. Same API shape, different arithmetic.
  • Adyen's fee is four separate components: commission, markup, scheme fees, interchange. Fee tax isn't provided at all. Its merchant references also arrive numeric-cast with a trailing ".0" that matches nothing until you strip it.
  • PayPal transaction ids mutate across API generations. Matching needed an array of candidate ids harvested out of Shopify's receipt JSON, because a single-key join silently loses everything old.
  • Shopify's "kept postage" on a refund exists only as free text in the refund note. We parsed "3.00 GBP postage fee applied" with a regex, with a fallback for small adjustment rows. It worked. I'm not proud.
  • Gift card orders: subtract the gift card amount from gross and add it to discount, or you invent a receivable that never existed.

Each of those rules cost us a mismatched period and a support thread with a finance operator who trusted us slightly less than the day before. This is also why the incumbent tools stop short: the A2X-class products only see what Shopify shows them, so third-party gateway fees are invisible upstream and the residue gets hidden in clearing accounts. Reading the provider's own settlement files is the whole game.

We could have rewritten the code in a quarter. This list took two years of being wrong in production, one provider at a time.


The aggregator trap

Do you build integrations yourself or rely on aggregators?

Codat and Merge promise a single API to dozens of accounting systems. Compelling pitch: why maintain 15 integrations when someone else will?

What the pitch doesn't mention: aggregators optimize for breadth, not depth. They give you data common across all systems, which means losing data specific to any one system. For reconciliation, specific data is the whole point. The field Xero calls one thing and QuickBooks calls another: that's where bugs live.

We tried aggregators first. Hit edge cases they couldn't handle. Built critical integrations ourselves. Ended up maintaining both: aggregator for long tail, custom for systems that mattered.

Aggregators are a shortcut to breadth but create a ceiling on depth. At some point you decide which systems are strategic and own those end-to-end. The middle ground, half aggregator and half custom, is worst of both worlds.


The upmarket trap

We started with SMBs. $5M–$50M revenue band where complexity has outpaced capacity but the business can't afford enterprise tooling. Systematically underserved.

Then we tried to go upmarket.

SMB sales: demo-to-close in weeks. Mid-market: demo-to-pilot-to-security-review-to-procurement-to-close in months. Same product, same pitch, entirely different process.

Our product could handle the technical requirements. Our company couldn't handle the sales cycle. Cash burn doesn't wait for procurement timelines. A 6-month enterprise deal is great with 18 months runway. Fatal with 9.

Going upmarket is about having enough capital to survive the sales cycle, not about having a better product. Product requirements are actually easier; enterprises know what they want and will tell you. The constraint is time.


Knowledge silos

Building a team from scratch means building everything from scratch. Not just product: culture, processes, ways of working that established companies take for granted.

First hire is terrifying: asking someone to bet their career on your idea. Fifth hire is terrifying: people's livelihoods now depend on decisions you're making with incomplete information.

What I underestimated: silos form immediately. Three people, and there's already information living in one head and nowhere else. Ten people, and whole subsystems only one person understands.

Team transitions make it worse. Someone leaves, takes context never written down because it seemed obvious. The person who knew why billing works that way is gone. Now you're reverse-engineering decisions from code comments and Slack threads.

The documentation you'll write later never gets written. The processes you'll formalize when you have time never get formalized. Knowledge transfer happens in hallway conversations you don't have when everyone's remote.


Taste vs. shipping

Hardest management problem: engineers who wanted to rebuild things that worked.

Often the existing code was genuinely bad. Built fast, with shortcuts, by people learning the domain while building. Any engineer with taste would want to refactor.

But refactoring doesn't ship features. Rebuilding auth doesn't close deals. The codebase making engineers cringe is running in production, serving customers, generating revenue.

The negotiation is constant: when does technical debt matter enough to pay down, versus when is it preference disguised as necessity? I got this wrong in both directions: shipping on systems about to collapse, approving rewrites that delivered no business value.

The codebase is never going to be good. Early-stage code is written under pressure, with incomplete information, by a team smaller than the problem requires. The goal is code that ships and doesn't break.


The over-engineering was mine

Taste versus shipping cuts both ways, and the biggest rebuild-that-shouldn't-have-happened was my own architecture. The pipeline ended up as extraction pods feeding an orchestrator feeding a warehouse feeding a transform layer feeding a mirror copied back into Postgres, because the app queried Postgres. Five systems. Every hop was individually defensible. I could justify each one in an architecture review and still couldn't tell you why a merchant's Tuesday numbers looked wrong without opening three consoles.

FIG 03The pipeline we ranfive systems, six tenants
EXTRACTIONone pod per connectionORCHESTRATORmanaged AirflowWAREHOUSEdataset per tenantTRANSFORMSper-tenant model graphMIRRORcopied back to PostgresAPPtenants served by all of this: sixalerting on any of it: none
Every hop was individually defensible. Together they meant nightly-batch freshness, a two-stack skillset, and no single place to ask "is it broken?" One database would have done it.

Two costs, in hindsight. Freshness: everything was nightly batch, so a 9am question got answered with yesterday's pipeline run, and the UI didn't say so. And observability: there was no alerting on any of it. Customers found failures before we did.

There was a subtler version of the same disease on the surface: integration breadth growing faster than integration truth. By the end, the journal had account codes for payment providers the pipeline never populated, and the integrations page had cards for systems no customer had connected. Half the surface was aspiration, and we were the ones maintaining it.


The pitch deck lies

Not intentionally; you can't fit full complexity into 12 slides. So you simplify. Tell the investable story. Leave out parts that are true but complicated.

Then you raise against that simplified story, and now you build the simplified version instead of the real one. The funded version is different from the version you know needs to exist.

Best investors understand this. They pattern-match on founders, not slides. But the process still selects for stories that fit the format: clear problem, solution, market, path to returns. Messy truth: you're figuring it out as you go, market is a guess, path to returns depends on things you can't control.

What I wish I'd known: terms matter more than valuation. Board composition matters more than check size. Small details during negotiation become constraints you live with for years.


Every payout tells two stories

One about cash, one about revenue. They happen on different days.

A customer needed this. Real customer, complex use case where "record it when it lands" would've been technically wrong and practically useless.

The complexity: each line item in a payout could represent a different product, different billing period, different revenue recognition rules. A single payout wasn't a transaction. It was a bundle of obligations, each maturing on its own schedule.

I expected the hard part to be accounting logic. Debits, credits, clearing accounts, period mapping. That's not what mattered.

What mattered: knowing what you don't know yet, until you run real data through it.

The counterintuitive thing about revenue recognition is the cardinality explosion at the line-item level. You think you're building one rule. You discover you're building N rules, where N is distinct product-period combinations in a single payout. You don't know what N looks like until real customer data tells you.

The other thing a payout bundles is currencies at two moments in time. Revenue legs convert at the order-date rate, because that's when you earned it. Cash legs convert at the capture-date rate, because that's when the money moved. The gap between the two anchors doesn't vanish. It's the FX gain or loss, and it deserves its own journal line rather than a shrug. Money that's been captured but not yet settled lives in the same gap; we eventually gave it a name and a page, cash in transit.

FIG 02Two stories of one payoutrevenue day ≠ cash day
ORDER DATECAPTUREPAYOUTREVENUE STORYsales · tax · discounts @ order-date rateCASH STORYnet · fees · fee tax @ capture-date ratethe gap between the two anchors = FX gain / losscaptured, not settled = cash in transitBANK
Revenue legs convert at the order-date rate; cash legs at the capture-date rate. The difference between the two anchors is the FX result, booked explicitly. The money living in the gap is cash in transit.

The details that are the point

Individual posting dates per line item. The alternative, one date per payout, collapses schedule into a point. You've thrown away information that makes revenue auditable. Each line item needs its own date because each is a separate recognition event. They just arrived together.

Pro-rata allocation by day count. Equal period slices are wrong the moment billing straddles months of unequal length. Day-count proration stays accurate regardless of calendar position. It's what auditors accept without question, defensible from first principles.

A rounding plug with a ceiling. Journals must balance, but real data leaves crumbs. Our rule: absorb an imbalance up to 0.05% of credits into an explicit Rounding line, and past that ceiling, ship the journal visibly unbalanced with a flag on it. The alternative, silently plugging whatever's missing, converts a data bug into a signed financial statement.

Golden numbers as the spec. The most useful tests we had were embarrassingly literal: this tenant, this fortnight, Sales equals exactly this number, to the cent. Vulgar as engineering, priceless as accounting. When a refactor moves a number, you want the test that tells you whose August just changed.

All of these prevent the same failure: someone asks "where did this number come from" and the system can't answer.

In fintech, explainability isn't transparency for its own sake. A number without a path back to source isn't a financial figure. It's an assertion. Drilldowns alone aren't enough. The real unlock is context with relevant action: showing someone why a number is what it is, then giving them something to do about it.


What I believe now

The product is the easy part. Code is the easy part. Hard part is everything else: team, capital, sales cycle, market timing, decisions made with incomplete information that can't be undone.

Complex processes only reveal themselves through real data. Prototyping is the discovery method. Skip it and you build the system you imagined, not the one actually needed.

Early stage is survival, not optimization. Right decision is the one keeping you alive long enough to make the next decision. Metrics that matter aren't in your investor update. They're runway, burn rate, and gap between what you promised and what you've shipped.

Barksdale was right: bundling and unbundling are the only moves. We were unbundling reconciliation from the ERP, making it standalone. Question was always whether we'd stay unbundled or get rebundled into something larger. That's the exit either way: bought by the bundle, or become the bundle yourself.

Ankor was the hands. The lesson was that hands need a body.


Postscript: what I'd tell the next founder

Added July 2026.

This summer I went back through everything we built — every repository, every era of the team — not to relitigate any of it, but to see what actually generalizes. The essay above is about the product. This is about building itself, written for whoever is where I was in 2023.

Convergence is validation. Over the years, different teams with different backgrounds, given real freedom, kept arriving at the same product shape: unified transactions, deterministic matching, clean journals. Nobody coordinated that. When independent, smart people keep converging on the same answer, the problem is real. I'd trust that signal over any single pilot or partner nod.

Verification is the scarce input now. Building has never been cheaper or faster, and it's only accelerating. What separates outcomes isn't how much gets built — it's whether anyone reliably checks. Review is a system, not a personality trait, and a team of individually excellent people can still operate without one. If you're a non-technical founder, you can't rent a technical conscience by the hour, but you can verify one: an hour a month in the git log tells you whether your quality controls actually operate. Make that someone's job. Even if it's yours.

Tie engineering ambition to commercial events. From inside engineering, every rebuild and re-platform is justifiable — I could defend each of ours in an architecture review. The discipline I'd add: no major technical investment without a named commercial condition attached to it. Not as bureaucracy, but as a forcing function. If you can't say what revenue event a rebuild unlocks, it's optionality you're buying with runway.

Ask pilots for commitment early. Paul Graham wrote that in a sense there's only one mistake that kills startups: not making something users want. We made something users wanted — they ran their month-end on it. What I learned is there's a second mistake hiding behind the first: not asking those users to pay while you still have time to act on the answer. A design partner who loves the product but won't sign a conditional contract is a reference, not a customer. Putting a contract on the table early feels like pushing your luck. It's actually the kindest test available.

Name your human dependencies. Small teams run on a handful of load-bearing people, and the org chart never shows which ones. Looking back, the moments that hurt most were never technical — they were transitions around people who held context nowhere else. The mitigation is boring and worth it: document, cross-train, keep one warm alternative for anything only one person understands.

Simplicity is a financial decision. Every layer of architecture beyond what your current scale requires is runway converted into optionality you don't own yet. I wrote earlier that the over-engineering was mine; the generalized version is that infrastructure choices are cash-flow choices, and the person who signs off on them should think like it.

We had genuinely talented people at every stage and backers who gave us real room to run. The gaps were systems — and systems are the founder's job. That's the least comfortable and most useful sentence in this whole essay.

If you're building right now and any of this maps to what you're seeing, I'd love to compare notes.

Kyle Rowley

Kyle Rowley

Builder & Ball Player

Email meLinkedInGitHubX (Twitter)