~/oybek.dev
Book a call
technology
Commerce· since 2018

Payments & checkout

I build payment and checkout flows that take money correctly: wallet checkout, Stripe, bank transfers — idempotent, webhook-driven, reconciled. Shipped on ChatFood and alfii.

Payments I've shipped

Payments are where a bug stops being cosmetic and starts costing real money. I've built and integrated them on production products, not on a sandbox.

On ChatFood — the order-and-pay platform acquired by Deliverect — I built the Apple Pay checkout on the Vue 3 frontend, the whole path from catalog to cart to a wallet tap. On the backend I contributed to the Laravel payment-links feature, the flow that lets a merchant send a customer a link and get paid without a full storefront.

On alfii, an HR and payroll SaaS, I integrated Stripe for card payments and Dapi for direct bank transfers. I did the analysis, set up the workflow, and wired both into a React front end. Along the way I moved the data layer from Redux-Saga to RTK Query, which made the payment and money-state flows far easier to reason about.

Abstract neon-green circuit traces converging on a single node over deep black, representing a checkout flow resolving to one charge
Payment flows: many paths in, one settled charge out.

Getting money right: integer cents, never floats

The most important rule in payments is the most boring one: never store money as a floating-point number. 0.1 + 0.2 is not 0.3 in floating-point, and once that rounding error lands in a balance it compounds into payouts that don't reconcile.

On alfii I hit exactly this — a money-handling bug rooted in float arithmetic. The fix was to move every monetary value to integer minor units: cents, fils, tiyin. You store 1099, not 10.99, and you only format to a decimal at the very edge, when you show it to a human. Math stays exact, totals reconcile, and the class of bug simply disappears.

This is non-negotiable in everything I build. Money is an integer count of the smallest unit of its currency, full stop.

Wallet checkout (Apple Pay)

The checkout I built for ChatFood was wallet-first, and the reason is conversion. Every field you ask a customer to type on a phone is a place they drop off. Apple Pay collapses name, card, billing, and shipping into one authenticated tap — biometric confirmation, no keyboard.

For food ordering, where the customer is hungry and on mobile, that matters more than almost anything else on the page. The engineering job is to make the wallet sheet appear at the right moment, hand the payment token to the backend cleanly, and handle the cases where the wallet isn't available without breaking the flow.

Wallet checkout also shrinks your PCI scope: the card data never touches your servers. That's a security win and a compliance win at the same time.

Bank transfers (Dapi)

Not every payment should run over a card. For payroll and larger B2B amounts, bank transfers are cheaper and often what the client actually wants. On alfii I integrated Dapi to move money bank-to-bank directly.

Bank rails behave differently from cards: they're asynchronous, they settle on their own schedule, and the source of truth is the webhook, not the API response you got back when you kicked the transfer off. Building this well means designing for that delay from the start — pending states the UI can show honestly, and a state machine that only marks money as received when the rail confirms it.

How I approach a payments integration

Every payments integration I build rests on a few hard rules, learned from shipping them.

Idempotency keys on every money-moving call. Networks retry, users double-click, mobile connections drop mid-request. An idempotency key means the same intent processed twice still charges once. Without it, you get duplicate charges and angry customers.

Webhooks are the source of truth, not the API response. A card auth can succeed, then the capture can fail; a transfer can be accepted, then reversed. The provider's webhook is the real event stream. I drive payment state from webhooks and treat the synchronous response as a hint, not a fact.

Reconciliation is part of the build, not an afterthought. At any moment I want to be able to answer: does what we think we collected match what the provider says it settled? If the two ledgers can't be compared, you find out about lost money weeks late.

Minimize PCI scope. I lean on hosted fields, wallet flows, and provider-hosted checkout so raw card data never lands on servers I'm responsible for. Less scope means less audit surface and less risk.

If you're building a commerce product that needs to take money, this is the foundation I start from.

Why I build with it

01

Money is an integer, never a float

I store every amount in the currency's smallest unit — cents, fils, tiyin — so totals stay exact and reconcile. On alfii I fixed a real money bug by moving to integer cents. Float arithmetic is how rounding errors creep into balances.

02

Idempotent and webhook-driven

Every money-moving call carries an idempotency key, so a retry or double-click charges once, not twice. Payment state is driven by the provider's webhooks — the real event stream — not the synchronous response, which can lie.

03

Wallet-first for conversion

Apple Pay and Google Pay collapse the whole checkout into one biometric tap, which converts far better on mobile. I built ChatFood's wallet checkout for exactly this — fewer fields typed means fewer customers lost.

04

Less PCI scope, less risk

I lean on hosted fields, wallet flows, and provider-hosted checkout so raw card data never touches servers I run. Smaller PCI scope means a smaller audit surface, less liability, and a more secure product by default.

Built with it

FAQ

Apple Pay or a normal card checkout?

Both, ideally — offer the wallet and keep a card form as fallback. Apple Pay (and Google Pay) convert better on mobile because they remove typing: one biometric tap replaces name, card number, and address. They also keep card data off your servers, which shrinks PCI scope. I built exactly this wallet-first checkout on ChatFood's Vue 3 frontend.

How do you store money values?

As integers in the currency's smallest unit — cents, fils, tiyin — never as floating-point. `10.99` becomes `1099`, and I only format to a decimal when displaying it to a person. On alfii I fixed a real money-handling bug by moving to integer cents. Float math introduces rounding errors that break reconciliation; integer minor units make the math exact.

Stripe or a local payment gateway?

It depends on where the money lands. Stripe is excellent for cards and gives you idempotency, webhooks, and hosted checkout out of the box — I integrated it on alfii. But for payroll, large B2B amounts, or markets where cards are expensive, direct bank transfers are often better; I used Dapi on alfii for exactly that. I pick the rail per use case, not by default.

How do you handle failed or duplicate charges?

With idempotency keys and webhook-driven state. Every money-moving request carries an idempotency key, so a retry or a double-click processes the same intent once instead of charging twice. Charge state is driven by the provider's webhooks — the real event stream — not the synchronous API response, which can lie. Failed charges surface honest pending and failed states in the UI rather than silent errors.

Can you add payments to an existing app?

Yes — that's usually how it happens. On alfii I added Stripe and Dapi into a running React app and did the full analysis and workflow design first. The work is integrating cleanly with your existing data layer, designing the money state machine, and wiring webhooks and reconciliation, not rebuilding your product around a checkout.

How do you keep payment data secure?

By keeping it off my servers wherever possible. I use hosted fields, wallet checkout (Apple Pay), and provider-hosted flows so raw card numbers never touch infrastructure I run, which minimizes PCI scope. Money state lives in integer minor units, every money-moving call is idempotent, and webhooks plus reconciliation make sure the ledgers always agree.

Also in Commerce

Have something like this in mind?

Start a project