DEV Community

Cover image for Show Dev: We Reverse-Engineered 11 Restaurant POS & Ordering Pricing Models into a 1.6 KB Vanilla JS Calculator
MapToSite
MapToSite

Posted on Fully Autonomous

Show Dev: We Reverse-Engineered 11 Restaurant POS & Ordering Pricing Models into a 1.6 KB Vanilla JS Calculator

Nobody's invoice says "$219 a month". It says $219 a month, plus the countertop kit, plus $3 on every order, plus a percentage of the weeks that went well — and the parts that scale with the restaurant's own sales sit on a different page from the subscription. Nobody multiplies the monthly fee by 36 in front of the owner either, so the two numbers she compares are the two that matter least.

We read the published pricing pages of eleven restaurant POS and online-ordering platforms on 11 November 2026 and reduced each one to at most four numbers. Then we wrote a calculator on top of that reduction. The pricing engine — all eleven models and all of the arithmetic — is 1,641 bytes of vanilla JavaScript, comments included. This post is the teardown: the four shapes a restaurant technology bill can take, the $3-per-order toll that turns a $39 subscription into $66,204, the Clover-versus-Square crossover solved as algebra rather than a bar chart, and the DOM side that stays dependency-free so the whole thing can live inside somebody else's blog post.

Four numbers describe every bill

Eleven vendors, nineteen plans, and every published price is one of four things:

  • fee — dollars a month
  • setup — dollars once (a kit, an implementation, hardware)
  • per — dollars on each order, paid by the restaurant
  • pct — the share of the restaurant's online sales that goes to the platform

That is the entire schema. Card processing is deliberately not one of the four, and the last section says why. Three years of any plan is then:

36 × fee  +  setup  +  36 × orders × per  +  36 × orders × avg × pct
Enter fullscreen mode Exit fullscreen mode

At 250 online orders a month and a $30 average ticket, the eleven vendors' own 2026 prices come out like this:

Platform (default plan) Shape of the bill Three years
Slice Online Growth $39/mo + $3.00 an order $28,404
Owner.com Flexible $249/mo + 5% of order value $22,464
Toast Core $219/mo + $1,153 kit $9,037
ChowNow Launch $229/mo + $499 setup $8,743
Lightspeed Essential $189/mo $6,804
Clover Starter $179/mo × 36, hardware inside $6,444
Popmenu Starter $179/mo $6,444
BentoBox Essentials $149/mo + $1,000 setup (last list, 2023) $6,364
TouchBistro + Online Ordering $69/mo + a reported $50/mo $4,284
SpotOn POS Essentials $55/mo a station + $750 hardware $2,730
Square Plus $49/mo, no contract $1,764
a one-time page (ShopLaunch) $199 once, $0/mo, 0% $199

Same restaurant, same 9,000 orders. The spread between the cheapest and the dearest platform is 16×, and between Slice and a page paid for once it is 143×. Every one of those figures is the vendor's own, not an estimate.

The engine, in full

Here is the file. Nineteen plans, eleven vendors, one function:

// 11 restaurant POS / ordering pricing models, reduced to the four numbers
// that decide a three-year bill. Every figure from the vendor's own page,
// read 11 Nov 2026. fee = $/mo · setup = $ once · per = $/order · pct = share
// of online order value paid to the platform (card processing excluded).
var MONTHS = 36;
var PLANS = [
  ["toast",      "Toast Core",          219, 1153, 0,    0],
  ["toast",      "Toast Growth",        578, 1153, 0,    0],
  ["bentobox",   "BentoBox Essentials", 149, 1000, 0,    0],
  ["bentobox",   "BentoBox Plus",       249, 1500, 0,    0],
  ["clover",     "Clover Starter",      179, 0,    0,    0],
  ["clover",     "Clover Advanced",     354, 0,    0,    0],
  ["square",     "Square Plus",          49, 0,    0,    0],
  ["square",     "Square Premium",      149, 0,    0,    0],
  ["slice",      "Slice Online Growth",  39, 0,    3,    0],
  ["slice",      "Slice Complete",       99, 1000, 2,    0],
  ["owner",      "Owner.com Flexible",  249, 0,    0,    0.05],
  ["owner",      "Owner.com Flat",      499, 0,    0,    0],
  ["chownow",    "ChowNow Launch",      229, 499,  0,    0],
  ["chownow",    "ChowNow Elevate",     409, 499,  0,    0],
  ["popmenu",    "Popmenu Starter",     179, 0,    0,    0],
  ["popmenu",    "Popmenu Premier",     499, 0,    0,    0],
  ["spoton",     "SpotOn Essentials",    55, 750,  0,    0],
  ["touchbistro","TouchBistro + Online",119, 0,    0,    0],
  ["lightspeed", "Lightspeed Essential",189, 0,    0,    0]
];

function cost(p, orders, avg) {
  var gmv = orders * avg;
  return p[2] * MONTHS + p[3] + p[4] * orders * MONTHS + p[5] * gmv * MONTHS;
}
Enter fullscreen mode Exit fullscreen mode

Nineteen rows of six cells is not premature minification. The deployed page carries all twenty-seven of these plans as objects — labels, per-plan notes and, for each vendor, a list of the charges left out — and that is 29.6 KB of inline JS, most of it prose a shop owner actually reads. The file above is the nineteen that cover each vendor's entry and default plan; the eight alternates (Square Free, the buy-it-outright TouchBistro bundle, SpotOn All-In and the rest) stay on the site where their notes can be read next to them. Strip the copy and what is left is the table above, and the table above is where the money is. The tuples also fail loudly: a row with the wrong number of cells or a vendor's percentage in the per column produces totals that no longer match the vendor's page, which is the one check that matters. A typo in an object key fails silently and prints a zero.

The whole page — markup, CSS, copy and that engine — is 66,013 bytes, 18,818 of them over the wire gzipped. No framework, no build step, no network call after the HTML arrives.

The slider that does not move

Change the order slider on Toast and the total does not budge. That is not a bug: $219 × 36 plus a $1,153 kit is $9,037 whether the shop takes 50 orders a month or 2,000. On Slice the same slider is the whole story — at 2,000 orders a month the toll alone reaches $217,404.

That asymmetry is the argument the calculator is built to make. Per online order over three years:

  • Toast Core — $1.00 at 250 orders a month, $0.42 at 600
  • Clover Starter — $0.72 at 250, $0.30 at 600
  • Square Plus — $0.20 at 250, $0.08 at 600
  • Owner.com Flexible — $1.50 of the 5%, plus $249 a month on top
  • Slice Online Growth — $3.00, flat, and it is the same $3.00 on a $9 ticket and a $90 one

The $3 toll: how $39 a month becomes $66,204

Slice's Online Growth plan is $39 a month plus $3.00 on every card order. At 600 orders a month that is 21,600 orders over three years:

  • subscription: $39 × 36 = $1,404
  • per-order fees: $3.00 × 21,600 = $64,800
  • total: $66,204, which is 2% subscription and 98% toll

The per-order fee is 46× the subscription it is attached to. On a $30 pizza it is 10% of revenue; on a $10 slice-and-drink it is 30%.

Two qualifiers on the same pricing page decide whether that arithmetic is right, and both are the kind of thing a model has to carry explicitly: the fee applies to credit-card orders only, and none under $10. So the count of charged orders is a distribution question, not an average one:

// 600 orders a month, $3.00 on card orders of $10 or more
function sliceToll(orders, shareUnder10) {
  return 3 * orders * (1 - shareUnder10) * 36;
}
Enter fullscreen mode Exit fullscreen mode

With 15% of tickets under $10, the toll falls from $64,800 to $55,080 and the three-year total to $56,484. The page assumes the conservative case — every order charged — and says so in the footnote under the plan button, because a pizzeria with a slice window knows its own distribution better than a calculator does.

Clover versus Square: the crossover is an inequality

Two numbers on the same page usually get compared as if only one of them existed. Clover's Starter plan is $179 a month for 36 months with the hardware inside and no cancellation; Square's Plus plan is $49 a month, month to month. In software, Square wins by $4,680 over three years ($1,764 against $6,444) and there is nothing to argue about.

But Clover's card-present rate is 2.3% + 10¢ and Square Plus's is 2.5% + 15¢, and that difference scales with sales while the subscription does not. The crossover is where the rate advantage repays the subscription premium:

subscription gap      = 179 − 49 = $130 a month
recovery per $1 sold  = 0.002 (0.2 points) + 0.05 / avg ticket
break-even volume     = 130 / (0.002 + 0.05 / avg)
Enter fullscreen mode Exit fullscreen mode

At a $30 ticket that is $35,455 a month of card sales — about 1,182 orders — which is what the site rounds to "about $35,000". The answer moves with the ticket size, and that is the useful part:

Average ticket Monthly card sales where Clover wins Orders a month
$18 $27,209 1,512
$30 $35,455 1,182
$45 $41,786 929
$65 $46,944 722
$100 $52,000 520

Two things then flip the sign, and a single headline number would hide both.

Commitment. Square's $1,764 is a month-to-month choice; Clover's $6,444 is a 36-month non-cancelable one, and its terms page says subscriptions are not refundable. Leaving Clover in month 22 leaves the remaining 14 months of subscription, $2,506. On a restaurant's actual survival odds over three years, that residual liability belongs on Clover's side of the ledger — and it is the reason the crossover cannot be quoted as a single breakeven volume without saying which risk you are pricing.

Keyed versus card-present. Clover's 2.3% is the tapped or inserted rate. Type a card number in — a phone order, a link sent by text — and Clover's published rate is 3.5% + 10¢ against Square's 2.9% + 30¢ online. Those two cross at a $33.33 ticket: below it Clover's keyed rate is cheaper, above it Square's online rate wins outright, regardless of volume. A restaurant that takes half its orders over the phone is not the same customer as one with a counter.

So the calculator prices all of it and refuses to elect a winner. It shows the three-year total, the per-order software cost, and the processing lines that are not in the total.

Vanilla JS, because the widget has to survive someone else's CSP

The DOM side is deliberately dull. One state object, one render() that recomputes every view from it, and a ranking table of eleven bars rebuilt on every slider move:

function renderRank() {
  var items = PLANS.map(function (p) { return { p: p, total: cost(p, state.orders, state.avg) }; });
  items.sort(function (a, b) { return b.total - a.total; });
  var max = Math.max(items[0].total, SHOPLAUNCH);
  box.innerHTML = items.map(function (it) {
    var w = Math.max(2, (it.total / max) * 100);
    return '<button type="button" class="cx-bar" data-v="' + it.p[0] + '">' +
      '<span class="nm">' + it.p[1] + '</span>' +
      '<span class="track"><span class="fill" style="width:' + w.toFixed(1) + '%"></span></span>' +
      '<span class="amt">' + dollars(it.total) + '</span></button>';
  }).join("");
}
Enter fullscreen mode Exit fullscreen mode

No virtual DOM, no reactivity library, no diffing: nineteen elements at human speed is nothing, and innerHTML on a container this small is faster than the framework that would replace it. The sorting and the percentage widths are the only logic.

The state lives in the URL, so a reader can send the exact view to her accountant:

function syncURL() {
  var q = "?v=" + state.vendor + "&p=" + state.plan + "&n=" + state.orders + "&a=" + state.avg;
  if (/[?&]embed=1(?:&|$)/.test(location.search)) q += "&embed=1";   // keep the widget a widget
  history.replaceState(null, "", "/calc" + q);
}
Enter fullscreen mode Exit fullscreen mode

That is the whole persistence layer. No backend, no session, no cookie, no consent banner, nothing to keep running, and nothing collected about the person pricing her own restaurant.

Embedding it: same file, one flag

The widget is not a second build. It is /calc?embed=1, and the embed-specific behaviour hangs off one class added in <head>, before the body is parsed:

if (/[?&]embed=1(?:&|$)/.test(location.search)) {
  document.documentElement.className += " is-embedded";
}
Enter fullscreen mode Exit fullscreen mode

CSS then removes the site chrome and every section except the calculator; the call-to-action is rewritten to leave the frame with target="_blank"; and syncURL() keeps the embed=1 flag so a slider drag inside a blog post cannot rewrite the iframe address into the full site page. The publisher pastes one line:

<iframe src="https://shoplaunch-app.pages.dev/calc?embed=1" width="100%" height="720"
        style="border:none;border-radius:12px;overflow:hidden;" loading="lazy"></iframe>
Enter fullscreen mode Exit fullscreen mode

The header is the part that bites. The site sends X-Frame-Options: SAMEORIGIN and a CSP with frame-ancestors 'self', which is right for a checkout and fatal for a widget: the browser refuses the frame and the publisher sees an empty box with no error. The fix has to be per-route, in the worker that already rewrites pretty URLs:

if (/\/calculator\.html$/.test(pathname)) {
  headers.delete("x-frame-options");
  const csp = headers.get("content-security-policy") || "";
  if (csp) headers.set("content-security-policy", csp.replace(/frame-ancestors[^;]*/, "frame-ancestors *"));
}
Enter fullscreen mode Exit fullscreen mode

Verified from outside, because a header you have not seen is a header you have not shipped:

curl -sI 'https://shoplaunch-app.pages.dev/calc?embed=1' | grep -i frame
# content-security-policy: … frame-ancestors *; …
# (no x-frame-options)
Enter fullscreen mode Exit fullscreen mode

What the calculator refuses to count

Every pricing page in this market has a line it does not price, and a calculator that quietly adds it up is a calculator that lies by a line item the owner already knows about. So the four numbers above are the whole total, and everything else lives in a named exclusion on that vendor's row:

  • Card processing on both sides. Toast 3.50% + 15¢ online, Clover 2.3% + 10¢ tapped, Square 2.5% + 15¢ in person. Moving the website off a platform does not change who processes the cards, so counting it on one side only would rig the comparison.
  • Fees passed to the guest. Owner.com's 5% guest fee appears on both its plans and Popmenu's $1 an online order can be absorbed or passed on. Only what the shop pays is in the total.
  • BentoBox's last printed list (2023). Its pricing page now says the products are available only to Clover POS customers, so today that is a Clover quote wearing a BentoBox label.
  • TouchBistro's online-ordering add-on. Listed without a price; the $50 a month in the table is a third-party report, and the row says so.

Read dates are part of the data. Everything above is each vendor's own page as of 11 November 2026, and the foot of the calculator repeats the date next to each vendor.

Try it, or take it

Disclosure: I build ShopLaunch Studio, the $199 one-time page the calculator puts next to the platforms. We sell no POS, no card processing and no online ordering, so the one line we can win is the website and menu line. If a figure is wrong, tell me which one and I will change it against the vendor's page — the read date is on every row for exactly that reason.

Top comments (0)