Skip to content

Vibe Coding a Store? Don't Vibe Code the Commerce Engine

An AI agent can build your store in an afternoon. Once it wraps an order around the payment, you own refunds, tax, disputes, and stock. Where to draw the line.
Build vs Buy16 min readBy Flint Pay, Product & API Team

Claude Code, Cursor, or Lovable will build a good-looking store in an afternoon, and a Stripe Checkout button in the hour after that. We recommend doing it; we publish a guide to selling online with an AI agent. The storefront is the part an agent is good at.

The trouble starts one prompt later. "Save the order so I can ship it" sounds like a small request, and the agent will answer it in a few minutes with a table and a webhook handler. That table is the first piece of a commerce engine: the system that decides what a buyer owes, records what they paid, and works out what they're owed back. Each prompt after it adds a feature that works in the demo and a responsibility that only shows up later. You're now holding other people's money, collecting tax on a state's behalf, and answering card-network disputes against a deadline.

Below, a store goes from the first buy button to a commerce engine, one reasonable prompt at a time. Each step covers what the agent builds, what it leaves out, and what you're now responsible for.

Why this part goes wrong with AI#

Generated code is not worse than handwritten code in general. Commerce code fails in specific ways that an agent's normal working loop doesn't catch.

The bugs aren't in the demo. An agent checks its work by running the app: click buy, pay with a test card, see the success page. Commerce bugs live in places that loop never reaches:

  • Two copies of the same webhook arriving at once.
  • A bank payment that fails three days after you shipped.
  • A refund of one item from an order that had a discount.
  • A dispute that arrives weeks after the sale.

You can't ask for what you don't know exists. The agent builds what the prompt describes. Nobody prompts "make fulfillment idempotent under concurrent webhook delivery" unless they already know Stripe can deliver the same event more than once, in any order. Reviewing generated code doesn't help either, because you can't spot a missing case you've never heard of.

Each prompt is local; the rules aren't. "Add discount codes" changes how totals are computed. It doesn't go back and change the refund code written last week, which still assumes the price on the line is what the buyer paid. Commerce rules cross features: a discount changes what a refund returns, and a refund changes what tax you owe. An agent working one request at a time will satisfy each request and break the rules that connect them.

Money bugs look correct. A broken layout looks wrong, so someone fixes it. A refund that returns 34 cents too much, tax collected at the wrong rate, or a unit sold twice all look like normal data. Someone finds them at month end, at tax filing time, or when a dispute needs evidence your database never kept.

From buy button to commerce engine, one prompt at a time#

1. "Add a buy button"#

The agent creates a Payment Link or a Checkout Session. Stripe hosts the payment page, emails a receipt, and keeps the record of the charge.

What you own: very little. Stripe holds the money record and the card data never touches your server. If your store sells one thing at one price and delivers it digitally, stop here. None of the steps below apply to you yet.

2. "Save the order so I can ship it"#

Now you need to know what was bought and who to ship it to. The agent adds an orders table and writes to it either from the success page or from a checkout.session.completed webhook.

What the first version usually misses:

  • The success page isn't guaranteed to load. Stripe's fulfillment guide says so: a buyer can pay and lose their connection before your page renders. Orders created there go missing.
  • "Completed" doesn't mean "paid." For bank debits and other delayed methods, the session completes while the payment is still processing and can still fail.
  • The handler runs more than once. Stripe documents that the same event can arrive more than once and that events arrive in no guaranteed order. It retries failed deliveries for up to three days in live mode. A handler that checks "does this order exist?" and then inserts will ship twice when two deliveries race.
  • Line items aren't in the event. The agent has to fetch them in a second request, which the line items post covers.

What you own: a second system of record. Stripe holds the truth about the money; your table holds the truth about the sale; a metadata string joins them. Every later question ("was this order refunded?") now needs both halves. Stripe as your source of truth walks through where each fact lives, and Next.js and Stripe: what the tutorials leave out has the handler code.

3. "Add a cart"#

The agent builds a cart in the browser and sends it to the server to create the Checkout Session. The common first version builds Stripe line items from what the browser sent, price included. Anyone who edits that request can set their own price.

What you own: a price authority. The server has to look up every price itself from a catalog it controls, which means you now maintain products, variants, and prices in your own database and keep them in sync with whatever Stripe needs. How to build a shopping cart with Stripe covers the limits (Checkout Sessions take up to 100 line items and one discount) and the server-cart pattern.

4. "Track stock so we don't oversell"#

The agent adds a quantity column and decrements it when the payment webhook arrives. That works until two buyers check out the last unit at the same time. Both see quantity 1 at checkout, both pay, and the webhook runs for both.

Decrementing at checkout instead doesn't fix it either. A Checkout Session stays open for 24 hours by default, so stock taken when the session opens is unavailable to everyone else for a day unless the buyer comes back.

What you own: reservations. Hold stock when checkout starts, release the hold when the session expires or the payment fails, commit it when the payment succeeds, and run a scheduled job that cleans up holds nobody released. That's three new states and a background job. Stripe inventory management lists the five race windows that oversell during a spike.

5. "Add discount codes"#

The first version is often one line:

TypeScript
const total = subtotal - subtotal * 0.1 + subtotal * TAX_RATE;

It passes every test you'd think to write and has three problems:

  • It uses floating point. JavaScript numbers are binary floating point, so 0.1 + 0.2 is 0.30000000000000004. Money needs integer cents, rounded once, at a defined step.
  • It taxes the undiscounted price. Most states tax the price after a discount the store itself gives, so this collects too much tax.
  • It doesn't record the discount per line. That doesn't matter until someone returns one item.

The per-line part is the one that grows. Say the order is three items at $9.99 and the code takes $10.00 off. Split evenly, each line gets 333.33 cents of discount, which you can't charge. Round each line and you've given away 999 cents, not 1,000. Someone has to get the extra cent, and you need to store which line got it. If the buyer returns that item, you refund $6.65 plus its tax. Return a different one and it's $6.66.

Then the follow-ups arrive: can two codes stack, does a percentage apply before or after a fixed amount, does "first order only" mean per email or per card, and does the code still apply if a refund drops the order under its minimum. Stripe Checkout accepts one discount per session, so stacking is something you build.

What you own: discount allocation stored per line, stacking rules, and the guarantee that refunds read the stored allocation instead of recomputing it.

6. "Charge sales tax"#

The agent multiplies by a rate or calls a tax API. Either way, you've taken on a legal role. A seller collects sales tax on the state's behalf, and in general the seller owes tax it should have collected, even when the checkout didn't charge it.

The code questions are smaller than the obligation, but they still need answers:

  • Per line or per order? Three items at $10.06 with 7% tax: tax each line and you collect 70 cents three times, $2.10. Tax the order and you collect $2.11. Pick one and store it, because refunds have to reverse exactly what you collected.
  • Is shipping taxable? It depends on the state.
  • Where does the rate come from? It depends on the buyer's address, which you might not have until checkout.

Then the part code can't do: deciding where you owe tax at all. Since the Supreme Court's 2018 Wayfair decision, states can require out-of-state sellers to collect once their sales into the state pass a threshold. Registering, filing, and paying are yours. What you're responsible for when you sell online covers the thresholds and filings.

What you own: a tax amount on every line, a rule for rounding it, a way to reverse it on refunds, and records good enough to file from.

7. "Let customers return one item"#

Stripe refunds take a payment and an amount. "Refund the blue shirt" is arithmetic the agent writes: the line's price, minus its stored share of the discount, plus its share of the tax, minus anything already refunded on that line. If step 5 didn't store the discount per line, this is where you find out. The agent will recompute the discount from today's rules, and the refund will be off by however much the rules have changed.

A returned item also raises questions the refund amount doesn't answer. Does the item go back into stock? Was shipping refunded? If the order was paid partly with a gift card, does the refund go to the card or the gift card? Is the order now partially refunded, and does your order history show that?

What you own: refund allocation per line, a refunded quantity on every line so nobody refunds the same item twice, and restocking rules. How to refund a line item in Stripe and Stripe partial refund: discount and tax reversal work through the math.

8. "Email receipts and show customers their orders"#

Stripe's receipt describes the charge. Your receipt has to describe the order: items, discounts, tax, shipping, and later the partial refund. Order history needs customer accounts, which need sign-in, which needs password resets or magic links. The Stripe customer portal won't help with one-time purchases, because it lists invoices, and one-time Checkout payments don't create them by default (why the portal doesn't show them).

What you own: receipt and refund emails that stay correct as the order changes, a buyer login, and the customer data that comes with it, including deletion requests.

9. "A customer disputed a charge"#

Nobody prompts for this one. It arrives as an email from your processor weeks after the sale. Stripe's documentation gives the shape of it:

  • You usually have 7 to 21 days to respond, depending on the card network. Miss the deadline and you lose automatically.
  • You get one submission. You can't add evidence afterward.
  • The bank can take up to 3 months to decide.

The evidence is the order: what was bought, at what price, under what refund policy, where it shipped, the tracking number, and every message with the buyer. If your order table holds a session ID and a status, you have little to send. A dispute fee applies whether or not you win, and a high dispute rate puts your account in card-network monitoring programs.

What you own: a record complete enough to win a dispute months later, a way to notice the deadline, and the money that leaves your balance in the meantime.

10. "What's in Tuesday's payout?"#

Your accountant asks why the bank deposit doesn't match the week's orders. Payouts arrive as one sum: payments minus fees, minus refunds, minus disputes, plus disputes won, across whichever days the processor batched. Matching a deposit to orders means walking the processor's balance transactions and joining each one back to a row in your table through the metadata key you wrote in step 2.

What you own: a reconciliation job, and the obligation to explain every difference between what you sold and what reached the bank.

The tally#

Ten prompts later, this is what the store is responsible for, and who finds out when it's wrong:

PromptWhat you now keep correctWho notices when it's wrong
Save the orderA second record of every sale, joined to the paymentYou, when an order goes missing or ships twice
Add a cartServer-side prices for every productYour margins, when someone edits a price
Track stockHolds, releases, and a cleanup jobA buyer, when you cancel their paid order
Discount codesInteger money, per-line discount allocation, stacking rulesYour accountant, when refunds don't add up
Sales taxPer-line tax, rounding rules, registrations and filingsA state tax agency
Return one itemRefund allocation, refunded quantities, restockingA buyer refunded too little, or you refunded too much
Receipts and historyOrder emails, buyer accounts, deletion requestsA buyer, a privacy regulator
DisputesEvidence and deadlines for every orderThe card networks and your processor
PayoutsMatching deposits to ordersYour accountant, at month end

None of these rows is hard on its own. The work is keeping all of them correct at once, as the rules interact, for as long as the store takes money.

How many states is that?#

Here's a way to see the size of it. Flint is a commerce API, so we had to write all of this down. A Flint order has five separate status fields, because each answers a different question:

FieldQuestionValues
statusCan the order still change?2
payment_statusHas the money arrived?3
fulfillment_statusHave the goods left?6
refund_statusHas money gone back?3
inventory_exception_statusDid stock fail after payment?2, when set

The first four alone give 108 combinations. Not all of them can happen, but every one that can is a case your support tools, emails, and refund rules have to handle. A paid order can sit unfulfilled for weeks; a fully refunded order stays paid, because money did arrive before it went back. A payment underneath has its own eight statuses, and a refund has four. Flint's API sends 232 webhook event types, 12 of them about orders alone. Statuses and lifecycles lists every value.

A vibe-coded store has the same states. It just doesn't name them. They end up as booleans like paid, shipped, and refunded, scattered across tables. refunded: true can't describe "one of three items refunded, tax reversed, still shipping the other two," so the code that reads it guesses.

How long things stay open#

Most of the work exists because a sale doesn't end when the buyer pays. These are the windows Stripe documents for a single card or bank payment:

What's still openFor how long
A Checkout Session holding stockUp to 24 hours by default
Webhook retries for one eventUp to 3 days in live mode
A bank debit that can still failDays, until the bank settles
A dispute response7 to 21 days from notice
A bank's decision on a disputeUp to 3 months

Your code has to be correct across all of those windows, after you've forgotten how it works. An agent doesn't remember either. Six months later it reads the same code you do, without the context of why the refund function reads a stored discount instead of recomputing it.

Where to draw the line#

Vibe coding is a good way to build a store. Here's how to split the work.

Keep vibe coding the storefront. Product pages, layout, search, content, landing pages, the cart UI, marketing emails, admin screens that read data, and integrations with your other tools. If a bug in it makes something look wrong, an agent can fix it the day you notice.

Don't vibe code the engine. That means the order record, price and total calculation, discount allocation, tax, payment state, refunds, stock holds, disputes, and reconciliation. If a bug in it changes how much someone owes, is owed, or receives, or what you owe a government, use an engine that already handles it.

You have three ways to get an engine:

OptionYou getYou still own
A hosted platform such as ShopifyEngine, storefront, and admin in one productWorking inside its themes, apps, and checkout rules; its fees
An open-source engine such as Medusa, Saleor, or VendureThe engine's code, already written and testedHosting, upgrades, the database, and connecting a payment processor
A commerce API such as FlintThe engine as an API, run for youThe storefront, which your agent builds against the API

Our comparisons with Shopify, Medusa, and Stripe go into each in more detail.

If you're building it anyway#

Sometimes building is the right call: one product, digital delivery, no stock, no partial refunds. If you're reviewing an agent's commerce code, check these first. Each one is a bug from the steps above.

CheckWhat correct looks like
PricesThe server looks up every price; the browser sends only IDs and quantities
MoneyInteger cents everywhere; rounding happens once, at a step you chose
Secret keysOnly on the server; nothing starting sk_ in the client bundle
Webhook signaturesVerified on the raw request body before anything else runs
Fulfillment triggerRuns on a paid status, not on "completed" or the success page
Duplicate eventsA unique constraint on the event or session ID, not a read-then-write check
Order recordCreated with its line items, linked to the payment by ID
StockHeld at checkout, released on expiry or failure, committed on payment
Discounts and taxStored per line at purchase time, never recomputed later
RefundsName a line and quantity, reverse that line's tax, and track refunded quantity

Then ask the agent for tests that cover what a demo can't: two webhook deliveries for one payment at the same moment, a bank payment that fails after checkout, and a partial refund on an order that had a discount. If those three pass, you're past the most common bugs.

What this looks like on Flint#

Flint is the third row in the options table: a commerce API where the order exists before the payment, and the payment settles against it. Your agent builds the storefront and sends line items. Flint computes totals, discounts, and tax on the server, holds stock when the order is paid and refuses the payment if nothing is left, and keeps payment, fulfillment, and refund status on the same order. The first prompt looks like this:

Text
Connect the Flint docs MCP server, then build me
a small store that sells three products through a
hosted checkout. Create orders on the server with
the sandbox key in my environment, pay one with a
test card, and show me the paid order at the end.

Fulfillment runs from one event, order.paid, which fires once the order is paid on any surface. Refunds take a line item and a quantity, and the tax on that line is reversed with it:

cURL
curl -X POST https://api.withflintpay.com/v1/refunds \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: refund-ord-1kmn0a-line-1" \
  -d '{
    "refund_type": "targets",
    "order_id": "ord_1kmn0aExample",
    "reason": "requested_by_customer",
    "line_items": [
      { "order_line_item_id": "li_1kmn0aExample", "quantity": 1,
        "tax_refund_mode": "automatic" }
    ]
  }'

Disputes arrive with their deadline as dispute.needs_response, and each payout lists the balance transactions inside it, with the order each one belongs to. Flint calculates and reports sales tax; registering and filing stay with you, as they would on any platform.

Sell online with AI walks through the full loop with an agent, and Add payments to a Lovable, v0, Bolt, or Replit app covers hosted AI builders.

Sources#

Everything attributed to Stripe above comes from its documentation, checked on 2026-10-22.

  • Fulfill orders: the success page isn't guaranteed to load; fulfillment can run more than once, concurrently; delayed payment methods.
  • Receive Stripe events in your webhook endpoint: duplicate deliveries, no guaranteed ordering, retries for up to three days in live mode.
  • Create a Checkout Session: sessions expire 30 minutes to 24 hours after creation, 24 hours by default; up to 100 line items; one discount per session.
  • Respond to disputes: the 7 to 21 day response window, a single submission, bank decisions taking up to 3 months, and the evidence categories.
  • Number on MDN: JavaScript numbers are double-precision binary floating point.
  • South Dakota v. Wayfair, Inc., 585 U.S. 162 (2018): states may require remote sellers to collect sales tax.