<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>BillPoints — Australian Bill Payment Points Calculator</title>

<meta name="description" content="BillPoints — compare net Qantas/Velocity points, cost-per-point and break-even fees for paying Australian bills (including ATO) via Sniip, B2Bpay, RewardPay and pay.com.au. Free, no signup." />

<meta property="og:title" content="BillPoints — AU Bill Payment Points Calculator" />

<meta property="og:description" content="Compare net points, fees and break-evens across Sniip, B2Bpay, RewardPay and pay.com.au. Handles ATO Amex tiered rates correctly." />

<meta property="og:type" content="website" />

<meta property="og:url" content="https://billpoints.com.au" />

<meta name="twitter:card" content="summary" />

<link rel="canonical" href="https://billpoints.com.au/" />

<link rel="preconnect" href="https://api.fontshare.com" />

<link href="https://api.fontshare.com/v2/css?f[]=general-sans@400,500,600,700&f[]=jetbrains-mono@400,500&display=swap" rel="stylesheet" />

<style>

:root {

--bg: #f6f5f1;

--surface: #ffffff;

--surface-2: #ecebe4;

--ink: #1a1a1a;

--ink-2: #4a4a48;

--ink-3: #86867f;

--line: #dedcd2;

--line-2: #c9c7bc;

--accent: #b8532e; /* burnt terracotta */

--accent-ink: #7a3419;

--positive: #2f6b3d;

--negative: #a83232;

--warn: #b0862a;

--amex: #1c4d8f;

--visa: #6b4a91;

--radius: 4px;

--radius-lg: 8px;

--font: 'General Sans', system-ui, sans-serif;

--mono: 'JetBrains Mono', ui-monospace, monospace;

}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body { background: var(--bg); color: var(--ink); font-family: var(--font); font-size: 15px; line-height: 1.5; -webkit-font-smoothing: antialiased; }

.wrap { max-width: 1180px; margin: 0 auto; padding: 32px 24px 80px; }


/* Header */

header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; padding-bottom: 24px; border-bottom: 1px solid var(--line); margin-bottom: 32px; }

.brand { display: flex; align-items: center; gap: 12px; }

.brand svg { flex-shrink: 0; }

.brand h1 { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }

.brand p { color: var(--ink-3); font-size: 13px; margin-top: 2px; }

.tag { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); background: var(--surface-2); padding: 4px 10px; border-radius: 100px; }


/* Layout */

.grid { display: grid; grid-template-columns: 380px 1fr; gap: 32px; align-items: start; }

@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }


/* Card */

.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 24px; }

.card h2 { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-3); margin-bottom: 18px; }

.card h3 { font-size: 14px; font-weight: 600; margin-bottom: 14px; letter-spacing: -0.005em; }


/* Inputs */

.field { margin-bottom: 18px; }

.field label { display: block; font-size: 13px; font-weight: 500; color: var(--ink-2); margin-bottom: 6px; }

.field .hint { font-size: 12px; color: var(--ink-3); margin-top: 4px; line-height: 1.4; }

.field .helper { margin-top: 6px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface-2); overflow: hidden; }

.field .helper summary { list-style: none; padding: 7px 10px; font-size: 11px; font-weight: 500; color: var(--accent-ink); cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; }

.field .helper summary::-webkit-details-marker { display: none; }

.field .helper summary::before { content: '?'; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); color: white; font-size: 10px; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }

.field .helper[open] summary { border-bottom: 1px solid var(--line); }

.field .helper .body { padding: 10px 12px 12px; font-size: 12px; color: var(--ink-2); line-height: 1.55; }

.field .helper .body p { margin-bottom: 8px; }

.field .helper .body p:last-child { margin-bottom: 0; }

.field .helper .body strong { color: var(--ink); font-weight: 600; }

.field .helper .body table { width: 100%; border-collapse: collapse; margin: 6px 0; font-size: 11px; }

.field .helper .body td { padding: 3px 6px 3px 0; vertical-align: top; border-bottom: 1px solid var(--line); }

.field .helper .body td:first-child { font-family: var(--mono); color: var(--accent-ink); white-space: nowrap; padding-right: 10px; }

.field .helper .body tr:last-child td { border-bottom: none; }

.input-wrap { position: relative; }

.input-wrap .prefix, .input-wrap .suffix { position: absolute; top: 50%; transform: translateY(-50%); font-family: var(--mono); font-size: 13px; color: var(--ink-3); pointer-events: none; }

.input-wrap .prefix { left: 12px; }

.input-wrap .suffix { right: 12px; }

input[type="number"], input[type="text"], select {

width: 100%; height: 40px; border: 1px solid var(--line-2); border-radius: var(--radius);

background: var(--surface); color: var(--ink); font-family: var(--mono); font-size: 14px;

padding: 0 12px; transition: border-color 0.15s, box-shadow 0.15s; outline: none;

}

input.has-prefix { padding-left: 26px; }

input.has-suffix { padding-right: 30px; }

input:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(184, 83, 46, 0.12); }

select { font-family: var(--font); appearance: none; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' stroke='%2386867f' stroke-width='1.5' fill='none' stroke-linecap='round'/></svg>"); background-repeat: no-repeat; background-position: right 12px center; padding-right: 32px; }


.seg { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; padding: 3px; background: var(--surface-2); border-radius: var(--radius); }

.seg button { border: none; background: transparent; padding: 8px 12px; font-family: var(--font); font-size: 13px; font-weight: 500; color: var(--ink-2); border-radius: 3px; cursor: pointer; transition: all 0.15s; }

.seg button.active { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgba(0,0,0,0.04); }

.seg button:not(.active):hover { color: var(--ink); }


/* Results */

.hero { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 24px; }

.hero .metric { background: var(--surface); padding: 20px 22px; }

.hero .metric .lbl { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); font-weight: 600; margin-bottom: 8px; }

.hero .metric .val { font-family: var(--mono); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; color: var(--ink); }

.hero .metric .sub { font-family: var(--mono); font-size: 12px; color: var(--ink-3); margin-top: 4px; }

.hero .metric.hi .val { color: var(--accent); }

.hero .metric.pos .val { color: var(--positive); }

.hero .metric.neg .val { color: var(--negative); }


@media (max-width: 640px) { .hero { grid-template-columns: 1fr; } }


/* Platform table */

.table-wrap { overflow-x: auto; margin: 0 -24px; }

table { width: 100%; border-collapse: collapse; font-size: 13px; }

th, td { text-align: left; padding: 12px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; white-space: nowrap; }

th:first-child, td:first-child { padding-left: 24px; white-space: normal; }

th:last-child, td:last-child { padding-right: 24px; }

th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); font-weight: 600; background: var(--surface-2); }

td { color: var(--ink-2); font-family: var(--mono); }

td.name { font-family: var(--font); color: var(--ink); font-weight: 500; }

td.num { text-align: right; }

th.num { text-align: right; }

tr.optimal { background: rgba(184, 83, 46, 0.05); }

tr.optimal td.name::before { content: ' '; color: var(--accent); font-weight: 700; }

tr.ineligible { opacity: 0.4; }

tr.ineligible td.num { color: var(--ink-3); }


/* Break-even bar */

.breakeven { padding: 18px 22px; background: var(--surface-2); border-radius: var(--radius); margin-top: 20px; }

.breakeven .row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }

.breakeven .row:last-child { margin-bottom: 0; }

.breakeven .lbl { font-size: 13px; color: var(--ink-2); }

.breakeven .v { font-family: var(--mono); font-size: 14px; font-weight: 500; color: var(--ink); }

.verdict { margin-top: 14px; padding: 12px 14px; border-radius: var(--radius); font-size: 13px; line-height: 1.5; }

.verdict.pos { background: rgba(47, 107, 61, 0.08); color: var(--positive); border-left: 3px solid var(--positive); }

.verdict.neg { background: rgba(168, 50, 50, 0.08); color: var(--negative); border-left: 3px solid var(--negative); }

.verdict.warn { background: rgba(176, 134, 42, 0.08); color: var(--warn); border-left: 3px solid var(--warn); }


/* Chart */

.chart-card { margin-top: 24px; }

.chart-wrap { position: relative; height: 260px; margin-top: 8px; }


/* Footer notes */

footer { margin-top: 32px; padding-top: 20px; border-top: 1px solid var(--line); font-size: 12px; color: var(--ink-3); line-height: 1.6; }

footer a { color: var(--ink-2); text-decoration: underline; text-decoration-color: var(--line-2); text-underline-offset: 2px; }

footer a:hover { color: var(--accent); }


/* Preset chips */

.presets { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }

.preset { font-family: var(--mono); font-size: 11px; padding: 4px 9px; background: var(--surface-2); border: 1px solid transparent; border-radius: 100px; cursor: pointer; color: var(--ink-2); transition: all 0.12s; }

.preset:hover { border-color: var(--line-2); color: var(--ink); }

</style>

</head>

<body>

<div class="wrap">

<header>

<div class="brand">

<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-label="Points return logo">

<rect x="1" y="1" width="30" height="30" rx="6" stroke="#1a1a1a" stroke-width="1.5"/>

<path d="M9 21L14 12L18 18L23 10" stroke="#b8532e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>

<circle cx="23" cy="10" r="2" fill="#b8532e"/>

</svg>

<div>

<h1>BillPoints</h1>

<p>Australian bill payment points calculator · Sniip · B2Bpay · RewardPay · pay.com.au</p>

</div>

</div>

<span class="tag">AU · 2026</span>

</header>


<div class="grid">

<!-- Left: Inputs -->

<aside class="card">

<h2>Your card &amp; spend</h2>


<div class="field">

<label>Card network</label>

<div class="seg" id="network-seg">

<button data-net="amex" class="active">American Express</button>

<button data-net="mcv">Mastercard / Visa</button>

</div>

<div class="hint">Amex earns more points but pays higher platform fees. Visa/MC is cheaper per transaction.</div>

</div>


<div class="field">

<label>Bill type</label>

<div class="seg" id="billtype-seg">

<button data-bt="nonato" class="active">Non-ATO bills</button>

<button data-bt="ato">ATO / super</button>

</div>

<div class="hint">ATO and super payments on <strong>any Amex card</strong> attract Sniip's tiered business rate (1.99% + GST at $0–99,999, dropping at higher tiers) — not the 1.29% personal rate. Visa/MC stays at 1.5% inc-GST. Full points still earned on ATO via Sniip.</div>

</div>


<div class="field">

<label for="spend">Annual bill spend routed via platform</label>

<div class="input-wrap">

<span class="prefix">$</span>

<input id="spend" type="number" class="has-prefix" value="10000" min="0" step="500" />

</div>

<div class="presets">

<span class="preset" data-spend="5000">$5k</span>

<span class="preset" data-spend="10000">$10k</span>

<span class="preset" data-spend="25000">$25k</span>

<span class="preset" data-spend="50000">$50k</span>

<span class="preset" data-spend="100000">$100k</span>

</div>

</div>


<div class="field">

<label>Card earns in</label>

<div class="seg" id="currency-seg">

<button data-cur="qantas" class="active">Qantas Points</button>

<button data-cur="velocity">Velocity Points</button>

</div>

<div class="hint">All results are normalised to this airline currency. Platform bonus points (Sniip Velocity, B2Bpay B2B Points) are converted 1:1 where the transfer partner exists.</div>

</div>


<div class="field">

<label for="earn">Base card earn rate (points per $1)</label>

<div class="input-wrap">

<input id="earn" type="number" value="1.25" min="0" step="0.05" />

</div>

<div class="presets">

<span class="preset" data-earn="0.5">0.5</span>

<span class="preset" data-earn="1">1.0</span>

<span class="preset" data-earn="1.25">1.25</span>

<span class="preset" data-earn="2.25">2.25 Plat</span>

<span class="preset" data-earn="3">3.0</span>

</div>

<details class="helper">

<summary>What earn rate should I use?</summary>

<div class="body">

<p>How many points your card earns per $1 <strong>when the transaction is processed by a bill-payment platform</strong> (not the headline rate on retail spend). Some cards drop to a lower tier on bill / BPAY / government transactions — check your card's rewards T&amp;Cs.</p>

<p><strong>Typical Australian rates on bill platforms:</strong></p>

<table>

<tr><td>0.5</td><td>Basic Visa/MC, HSBC Platinum Qantas</td></tr>

<tr><td>0.75</td><td>Qantas Amex Discovery, ANZ/Westpac Qantas Black</td></tr>

<tr><td>1.0</td><td>Qantas Amex Premium, NAB Qantas Signature, Citi Prestige</td></tr>

<tr><td>1.25</td><td>Amex Explorer, Qantas Amex Ultimate</td></tr>

<tr><td>2.0</td><td>Amex Business Explorer, Velocity Business Flyer</td></tr>

<tr><td>2.25</td><td>Amex Platinum Charge, Velocity Platinum Business</td></tr>

</table>

<p><strong>Not sure?</strong> Divide the points you got on your last statement by the dollars you spent through Sniip / B2Bpay that same month. Or use the lower end — overstating this input makes platforms look better than they are.</p>

</div>

</details>

</div>


<div class="field">

<label for="bonus">Bonus points available (one-off)</label>

<div class="input-wrap">

<input id="bonus" type="number" value="0" min="0" step="500" />

</div>

<div class="hint">One-off promotional bonus applied to your total (e.g. current Amex Sniip offer: 1,250 bonus pts per $250 spend, up to 3,750 total). Leave 0 if none apply.</div>

</div>


<div class="field">

<label for="ppv">Your value per point (cents)</label>

<div class="input-wrap">

<input id="ppv" type="number" value="1.8" min="0" step="0.1" />

<span class="suffix">¢</span>

</div>

<div class="presets">

<span class="preset" data-ppv="1.0">1.0¢</span>

<span class="preset" data-ppv="1.3">1.3¢</span>

<span class="preset" data-ppv="1.5">1.5¢</span>

<span class="preset" data-ppv="1.8">1.8¢</span>

<span class="preset" data-ppv="2.2">2.2¢</span>

</div>

<details class="helper">

<summary>What's a realistic point value?</summary>

<div class="body">

<p>How much <strong>you</strong> get per point when you actually redeem — not what the airline markets it at (~1¢). This is the most important input; it decides whether a platform's fee is worth the points.</p>

<p><strong>Rough consensus values:</strong></p>

<table>

<tr><td>0.8–1.0¢</td><td>Cash-equivalent: Qantas Store, gift cards, fuel discount. The floor.</td></tr>

<tr><td>1.2–1.5¢</td><td>Domestic economy award flights, credit toward paid Qantas flights.</td></tr>

<tr><td>1.5–1.9¢</td><td>Domestic business or international economy award flights.</td></tr>

<tr><td>1.8–2.2¢</td><td>International business class on QF, VA or partners when award seats are available.</td></tr>

<tr><td>2.5–4¢+</td><td>First class / long-haul business. Hard to consistently achieve.</td></tr>

</table>

<p><strong>Quick rule of thumb:</strong> use the value of the redemption you'll most realistically take, not your aspirational one. If you mostly cash out for gift cards, use 1.0¢. If you fly domestic economy on points, use 1.3–1.5¢.</p>

</div>

</details>

</div>


<div class="field">

<label for="fee">Card annual fee</label>

<div class="input-wrap">

<span class="prefix">$</span>

<input id="fee" type="number" class="has-prefix" value="0" min="0" step="10" />

</div>

<div class="hint">Your card's annual fee (e.g. $395 for Amex Explorer, $1,450 for Amex Platinum Charge). Leave $0 if the fee is already offset by other card benefits, or you want to see pure platform economics.</div>

</div>

</aside>


<!-- Right: Results -->

<main>

<div class="hero" id="hero">

<div class="metric hi"><div class="lbl">Net points earned</div><div class="val" id="m-points">—</div><div class="sub" id="m-points-sub"></div></div>

<div class="metric"><div class="lbl">Cost per point</div><div class="val" id="m-cpp">—</div><div class="sub" id="m-cpp-sub"></div></div>

<div class="metric"><div class="lbl">Net value</div><div class="val" id="m-net">—</div><div class="sub" id="m-net-sub"></div></div>

</div>


<section class="card">

<h3>Platform comparison for your spend</h3>

<div class="table-wrap">

<table id="table">

<thead>

<tr>

<th>Platform</th>

<th class="num">Fee %</th>

<th class="num">Fee $</th>

<th class="num">Card pts</th>

<th class="num">Platform bonus</th>

<th class="num" id="th-total">QF total</th>

<th class="num">CPP</th>

<th class="num">Net value</th>

</tr>

</thead>

<tbody id="tbody"></tbody>

</table>

</div>


<div class="breakeven" id="be">

<div class="row"><span class="lbl">Break-even fee % (fee = point value)</span><span class="v" id="be-pct">—</span></div>

<div class="row"><span class="lbl">Your best platform's fee</span><span class="v" id="be-best">—</span></div>

<div class="row"><span class="lbl">Headroom (fee ceiling − platform fee)</span><span class="v" id="be-head">—</span></div>

<div class="verdict" id="verdict"></div>

</div>

</section>


<section class="card chart-card">

<h3>Net value across annual spend</h3>

<div class="chart-wrap"><canvas id="chart"></canvas></div>

</section>


<footer>

<strong style="color:var(--ink-2)">Fee data as of Sept 2026 · Verify current rates before large payments.</strong><br/>

Some cards cap or exclude points on BPAY/government payments — check your issuer's terms. ATO payments on any Amex use tiered business rates (from 1.75%+GST) on Sniip.


<div style="margin-top:20px;padding-top:16px;border-top:1px solid var(--line)">

<div style="font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:var(--ink-2);font-weight:600;margin-bottom:10px">Sources</div>


<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px 24px">

<div>

<div style="font-size:12px;color:var(--ink-2);font-weight:500;margin-bottom:6px">Fees &amp; pricing</div>

<ul style="list-style:none;padding:0;margin:0;font-size:12px;line-height:1.8">

<li>Sniip — <a href="https://sniip.com/faq/" target="_blank" rel="noopener">FAQ &amp; fees</a>, <a href="https://sniip.com/personal-payments/" target="_blank" rel="noopener">personal payments</a>, <a href="https://sniip.com/american-express-payments/" target="_blank" rel="noopener">Amex payments</a></li>

<li>B2Bpay — <a href="https://www.b2bpay.com.au/points/" target="_blank" rel="noopener">tiers &amp; fees</a></li>

<li>RewardPay — <a href="https://www.rewardpay.com.au/pricing" target="_blank" rel="noopener">pricing &amp; accelerators</a></li>

<li>pay.com.au — <a href="https://www.pay.com.au/pricing" target="_blank" rel="noopener">pricing</a></li>

<li>Payment Logic — <a href="https://www.paymentlogic.com.au/pricing/" target="_blank" rel="noopener">pricing</a></li>

</ul>

</div>

<div>

<div style="font-size:12px;color:var(--ink-2);font-weight:500;margin-bottom:6px">Bonus points evidence</div>

<ul style="list-style:none;padding:0;margin:0;font-size:12px;line-height:1.8">

<li>Sniip 1 Velocity pt / $10 — <a href="https://sniip.com/virgin-australia-business-flyer/" target="_blank" rel="noopener">Virgin Business Flyer page</a> · <a href="https://www.velocityfrequentflyer.com/business-flyer/partners-and-offers/cards-payments-transactions/sniip" target="_blank" rel="noopener">Velocity T&amp;Cs</a></li>

<li>B2Bpay Earn Plus (1 B2B pt / $2) &amp; Earn Max (1 B2B pt / $1) — <a href="https://www.b2bpay.com.au/points/" target="_blank" rel="noopener">B2Bpay Points</a></li>

<li>RewardPay Qantas <em>and</em> Velocity accelerators (1pt/$4 to 2pts/$1, same fee tiers) — <a href="https://www.rewardpay.com.au/pricing" target="_blank" rel="noopener">RewardPay Pricing</a></li>

<li>B2B Qantas / Velocity 1:1 transfer — <a href="https://www.b2bpay.com.au/points/" target="_blank" rel="noopener">B2Bpay conversion partners</a></li>

</ul>

</div>

</div>


<div style="margin-top:12px;font-size:12px">

<div style="font-size:12px;color:var(--ink-2);font-weight:500;margin-bottom:6px">Independent comparison &amp; guidance</div>

<ul style="list-style:none;padding:0;margin:0;font-size:12px;line-height:1.8">

<li><a href="https://www.pointhacks.com.au/best-b2b-payment-platforms-in-australia-rewards/" target="_blank" rel="noopener">Point Hacks — Best B2B payment platforms in Australia</a></li>

<li><a href="https://www.australianfrequentflyer.com.au/credit-cards/earn-points-paying-tax-bills/" target="_blank" rel="noopener">Australian Frequent Flyer — Earn points paying ATO tax &amp; bills</a></li>

</ul>

</div>

</div>


<div style="margin-top:16px;padding-top:14px;border-top:1px solid var(--line);color:var(--ink-3);font-size:11px;line-height:1.5">

<strong style="color:var(--ink-2)">Disclosure:</strong> Links to platforms above may be affiliate or referral links — we may earn a small commission at no extra cost to you if you sign up through them. Bonus-evidence and pricing links point directly to each platform's own documentation and are non-commercial. This calculator is provided for information only and is not financial or credit advice. Verify current fees, promotions and card issuer terms before making any payment. Points-earning rules change frequently and issuers may exclude specific merchants, BPAY, or ATO payments from earning points without notice.

</div>

</footer>

</main>

</div>

</div>


<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>

<script>

// ---- Platform fee data (Sep 2026, incl GST where applicable) ----

// Bonus points model:

// bonusPer$ = platform-currency points earned per $1 of bill (on top of card points)

// bonusCurrency = 'velocity' | 'qantas' | 'b2b' (B2B Points convert 1:1 to QF or VF)

// bonusEligibleAmex = whether bonus applies to Amex on this platform

// Fee percentages below are ex-GST rates converted to inc-GST (rate × 1.10).

// Bonus rates are the *additional* platform fee (ex GST) charged when opting into that bonus tier;

// they add on top of the base card processing fee.

// ---- Affiliate / referral links ----

// Placeholder URLs — swap the `?ref=...` for real affiliate IDs once approved by

// Commission Factory / Impact.com / direct partner program.

// Any click here should count as an outbound referral event.

const PLATFORM_LINKS = {

'Sniip': 'https://sniip.com/?ref=billpoints',

'B2Bpay': 'https://www.b2bpay.com.au/?ref=billpoints',

'RewardPay': 'https://www.rewardpay.com.au/?ref=billpoints',

'pay.com.au': 'https://pay.com.au/?ref=billpoints',

'Payment Logic': 'https://paymentlogic.com.au/?ref=billpoints',

};


const PLATFORMS = [

{

name: 'Sniip',

tier: 'Standard',

amex: { fee: 0.0129, note: 'Personal Amex (non-ATO)' },

mcv: { fee: 0.0150, note: 'Personal Visa/MC' },

bonusPer$: 0.10,

bonusCurrency: 'velocity',

bonusEligibleAmex: false,

bonusFeeAdd: 0,

bonusSourceUrl: 'https://sniip.com/virgin-australia-business-flyer/',

bonusSourceLabel: 'Sniip · Velocity Business Flyer',

},

{

name: 'B2Bpay',

tier: 'Essential',

amex: { fee: 0.02420, note: '2.2% + GST' },

mcv: { fee: 0.01452, note: '1.32% + GST std' },

bonusPer$: 0,

bonusCurrency: 'b2b',

bonusEligibleAmex: true,

bonusFeeAdd: 0,

},

{

name: 'B2Bpay',

tier: 'Earn Plus',

amex: { fee: 0.02420, note: 'Amex flat 2.2% + GST' },

mcv: { fee: 0.02310, note: 'V/MC 2.10% + GST' },

bonusPer$: 0.50, // 1 B2B pt / $2 = 0.50 pt/$1

bonusCurrency: 'b2b',

bonusEligibleAmex: true,

bonusFeeAdd: 0, // fee already includes the Earn Plus tier rate

bonusSourceUrl: 'https://www.b2bpay.com.au/points/',

bonusSourceLabel: 'B2Bpay · Earn Plus (B2B Points)',

},

{

name: 'B2Bpay',

tier: 'Earn Max',

amex: { fee: 0.04235, note: 'Amex 3.85% + GST' },

mcv: { fee: 0.02750, note: 'V/MC 2.50% + GST' },

bonusPer$: 1.00, // 1 B2B pt / $1

bonusCurrency: 'b2b',

bonusEligibleAmex: true,

bonusFeeAdd: 0,

bonusSourceUrl: 'https://www.b2bpay.com.au/points/',

bonusSourceLabel: 'B2Bpay · Earn Max (B2B Points)',

},

{

name: 'RewardPay',

tier: 'Standard',

amex: { fee: 0.02365, note: '2.15% + GST' },

mcv: { fee: 0.01375, note: '1.25% + GST' },

bonusPer$: 0,

bonusCurrency: 'qantas',

bonusEligibleAmex: true,

bonusFeeAdd: 0,

},

{

name: 'RewardPay',

tier: '+ QF 1pt/$4',

amex: { fee: 0.02365 + 0.0055, note: '2.15% + 0.50% + GST' },

mcv: { fee: 0.01375 + 0.0055, note: '1.25% + 0.50% + GST' },

bonusPer$: 0.25,

bonusCurrency: 'qantas',

bonusEligibleAmex: true,

bonusFeeAdd: 0.0055,

bonusSourceUrl: 'https://www.rewardpay.com.au/pricing',

bonusSourceLabel: 'RewardPay · Qantas accelerator 1pt/$4',

},

{

name: 'RewardPay',

tier: '+ QF 1pt/$2',

amex: { fee: 0.02365 + 0.01089, note: '2.15% + 0.99% + GST' },

mcv: { fee: 0.01375 + 0.01089, note: '1.25% + 0.99% + GST' },

bonusPer$: 0.50,

bonusCurrency: 'qantas',

bonusEligibleAmex: true,

bonusFeeAdd: 0.01089,

bonusSourceUrl: 'https://www.rewardpay.com.au/pricing',

bonusSourceLabel: 'RewardPay · Qantas accelerator 1pt/$2',

},

{

name: 'RewardPay',

tier: '+ QF 1pt/$1',

amex: { fee: 0.02365 + 0.0198, note: '2.15% + 1.80% + GST' },

mcv: { fee: 0.01375 + 0.0198, note: '1.25% + 1.80% + GST' },

bonusPer$: 1.00,

bonusCurrency: 'qantas',

bonusEligibleAmex: true,

bonusFeeAdd: 0.0198,

bonusSourceUrl: 'https://www.rewardpay.com.au/pricing',

bonusSourceLabel: 'RewardPay · Qantas accelerator 1pt/$1',

},

{

name: 'RewardPay',

tier: '+ QF 2pts/$1',

amex: { fee: 0.02365 + 0.0396, note: '2.15% + 3.60% + GST' },

mcv: { fee: 0.01375 + 0.0396, note: '1.25% + 3.60% + GST' },

bonusPer$: 2.00,

bonusCurrency: 'qantas',

bonusEligibleAmex: true,

bonusFeeAdd: 0.0396,

bonusSourceUrl: 'https://www.rewardpay.com.au/pricing',

bonusSourceLabel: 'RewardPay · Qantas accelerator 2pts/$1',

},

{

name: 'RewardPay',

tier: '+ VF 1pt/$4',

amex: { fee: 0.02365 + 0.0055, note: '2.15% + 0.50% + GST' },

mcv: { fee: 0.01375 + 0.0055, note: '1.25% + 0.50% + GST' },

bonusPer$: 0.25,

bonusCurrency: 'velocity',

bonusEligibleAmex: true,

bonusFeeAdd: 0.0055,

bonusSourceUrl: 'https://www.rewardpay.com.au/pricing',

bonusSourceLabel: 'RewardPay · Velocity accelerator 1pt/$4',

},

{

name: 'RewardPay',

tier: '+ VF 1pt/$2',

amex: { fee: 0.02365 + 0.01089, note: '2.15% + 0.99% + GST' },

mcv: { fee: 0.01375 + 0.01089, note: '1.25% + 0.99% + GST' },

bonusPer$: 0.50,

bonusCurrency: 'velocity',

bonusEligibleAmex: true,

bonusFeeAdd: 0.01089,

bonusSourceUrl: 'https://www.rewardpay.com.au/pricing',

bonusSourceLabel: 'RewardPay · Velocity accelerator 1pt/$2',

},

{

name: 'RewardPay',

tier: '+ VF 1pt/$1',

amex: { fee: 0.02365 + 0.0198, note: '2.15% + 1.80% + GST' },

mcv: { fee: 0.01375 + 0.0198, note: '1.25% + 1.80% + GST' },

bonusPer$: 1.00,

bonusCurrency: 'velocity',

bonusEligibleAmex: true,

bonusFeeAdd: 0.0198,

bonusSourceUrl: 'https://www.rewardpay.com.au/pricing',

bonusSourceLabel: 'RewardPay · Velocity accelerator 1pt/$1',

},

{

name: 'RewardPay',

tier: '+ VF 2pts/$1',

amex: { fee: 0.02365 + 0.0396, note: '2.15% + 3.60% + GST' },

mcv: { fee: 0.01375 + 0.0396, note: '1.25% + 3.60% + GST' },

bonusPer$: 2.00,

bonusCurrency: 'velocity',

bonusEligibleAmex: true,

bonusFeeAdd: 0.0396,

bonusSourceUrl: 'https://www.rewardpay.com.au/pricing',

bonusSourceLabel: 'RewardPay · Velocity accelerator 2pts/$1',

},

{

name: 'pay.com.au',

tier: 'Standard',

amex: { fee: 0.01925, note: '1.75% + GST' },

mcv: { fee: 0.01100, note: '1.0% + GST' },

bonusPer$: 0,

bonusCurrency: 'qantas',

bonusEligibleAmex: true,

bonusFeeAdd: 0,

},

{

name: 'Payment Logic',

tier: 'Standard',

amex: { fee: 0.02365, note: '2.15% + GST' },

mcv: { fee: 0.01375, note: '1.25% + GST' },

bonusPer$: 0,

bonusCurrency: 'qantas',

bonusEligibleAmex: true,

bonusFeeAdd: 0,

},

];


// Convert bonus-currency points user's chosen airline currency (Qantas or Velocity)

// 1:1 for QFQF, VFVF, B2BQF, B2BVF. Cross-airline (QFVF) not directly possible — mark as ineligible.

function convertBonus(bonusPts, bonusCurrency, targetCurrency) {

if (bonusPts === 0) return { pts: 0, converted: false, ineligible: false };

if (bonusCurrency === targetCurrency) return { pts: bonusPts, converted: false, ineligible: false };

if (bonusCurrency === 'b2b') return { pts: bonusPts, converted: true, ineligible: false }; // 1:1 to QF or VF

// Sniip's Velocity bonus can't convert to Qantas, and vice versa

return { pts: 0, converted: false, ineligible: true };

}


// RewardPay is Amex-only historically but now supports Visa/MC per 2026 update. Keep as available.


// ---- State ----

// ---- Referral click tracking ----

// Sends a click event to Plausible/GA/Cloudflare if any is present.

// Silent no-op if analytics hasn't loaded yet, so the link still opens normally.

function trackReferral(platform, tier) {

try {

// Plausible (custom events — requires analytics.pplx-hosted or plausible.io script)

if (typeof window.plausible === 'function') {

window.plausible('Referral click', { props: { platform, tier } });

}

// Google Analytics 4 (gtag) fallback

if (typeof window.gtag === 'function') {

window.gtag('event', 'referral_click', { platform, tier });

}

// Local in-memory fallback — kept on window for DevTools inspection.

window.__pc_clicks = window.__pc_clicks || [];

window.__pc_clicks.push({ platform, tier, t: Date.now() });

if (window.__pc_clicks.length > 200) window.__pc_clicks.shift();

} catch (e) { /* swallow */ }

}


const state = {

network: 'amex',

currency: 'qantas', // 'qantas' | 'velocity'

billType: 'nonato', // 'nonato' | 'ato'

spend: 10000,

earn: 1.25,

bonus: 0,

ppv: 1.8, // cents per point

fee: 0,

};


// Sniip ATO / super tiered business rate for ANY Amex card.

// Rates are ex-GST (from https://sniip.com/faq/); inc-GST = ex × 1.10.

// Tier is picked by the SINGLE payment amount, not annual spend.

function sniipAtoAmexFeeIncGST(paymentAmount) {

const p = paymentAmount || 0;

let ex;

if (p >= 1000000) ex = 0.0175;

else if (p >= 500000) ex = 0.0185;

else if (p >= 300000) ex = 0.0190;

else if (p >= 100000) ex = 0.0195;

else ex = 0.0199;

return ex * 1.10;

}

function sniipAtoAmexTierLabel(paymentAmount) {

const p = paymentAmount || 0;

if (p >= 1000000) return '$1M+ tier: 1.75% + GST';

if (p >= 500000) return '$500k–999k tier: 1.85% + GST';

if (p >= 300000) return '$300k–499k tier: 1.90% + GST';

if (p >= 100000) return '$100k–299k tier: 1.95% + GST';

return '$0–99k tier: 1.99% + GST';

}


const CURRENCY_LABEL = { qantas: 'Qantas Points', velocity: 'Velocity Points' };

const CURRENCY_SHORT = { qantas: 'QF', velocity: 'VF' };


// ---- Helpers ----

const fmt$ = (n) => (n < 0 ? '-$' : '$') + Math.abs(n).toLocaleString(undefined, { maximumFractionDigits: 0 });

const fmt$2 = (n) => (n < 0 ? '-$' : '$') + Math.abs(n).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });

const fmtPct = (n, d=2) => (n*100).toFixed(d) + '%';

const fmtPts = (n) => Math.round(n).toLocaleString();

const fmtCPP = (n) => n.toFixed(2) + '¢';


function calcForPlatform(p) {

// ATO override: Sniip charges tiered business rates on ANY Amex for ATO / super.

// Visa/MC ATO fee is unchanged (1.5% inc-GST). Other platforms are unaffected — B2Bpay,

// pay.com.au, RewardPay, and Payment Logic don't process ATO payments the same way.

let feePct, note;

if (state.billType === 'ato' && p.name === 'Sniip' && state.network === 'amex') {

feePct = sniipAtoAmexFeeIncGST(state.spend);

note = 'ATO Amex · ' + sniipAtoAmexTierLabel(state.spend);

} else {

feePct = p[state.network].fee;

note = p[state.network].note;

}

const feeDollars = state.spend * feePct;

const chargedAmount = state.spend + feeDollars;

// Card earns on the full charged amount (bill + fee) per Sniip / typical platform rules.

// Sniip confirms FULL points earned on ATO payments via Sniip (unlike direct ATO card payments

// which often earn zero or reduced points on Amex).

const cardPts = chargedAmount * state.earn;


// Raw platform bonus (in its native currency), before conversion

let rawBonusPts = 0;

let bonusEligible = false;

if (p.bonusPer$ > 0) {

bonusEligible = state.network === 'mcv' || (state.network === 'amex' && p.bonusEligibleAmex);

// Sniip's Velocity bonus tier is a non-ATO personal-payment offer — disable it for ATO.

if (state.billType === 'ato' && p.name === 'Sniip') bonusEligible = false;

if (bonusEligible) rawBonusPts = state.spend * p.bonusPer$;

}

// Convert to chosen airline currency

const conv = convertBonus(rawBonusPts, p.bonusCurrency, state.currency);

const platformBonusPts = conv.pts;

const bonusIneligible = conv.ineligible;


const totalPts = cardPts + platformBonusPts + state.bonus;

const totalCost = feeDollars + state.fee;

const pointValue = totalPts * (state.ppv / 100);

const netValue = pointValue - totalCost;

const cpp = totalPts > 0 ? (totalCost / totalPts) * 100 : Infinity;

return {

name: p.name, feePct, feeDollars, cardPts,

platformBonusPts, rawBonusPts, bonusCurrency: p.bonusCurrency, bonusConverted: conv.converted, bonusIneligible,

bonusSourceUrl: p.bonusSourceUrl, bonusSourceLabel: p.bonusSourceLabel,

totalPts, totalCost, pointValue, netValue, cpp, note

};

}


function render() {

const rows = PLATFORMS.map(calcForPlatform)

// Hide airline-mismatched accelerator variants:

// if a row's bonusCurrency is fixed to one airline (qantas/velocity) AND doesn't match

// the user's chosen currency, AND the row actually paid an extra fee for that bonus,

// it's just noise — hide it. Rows with bonusPer$ === 0 (Standard/base tiers) always show.

.filter(r => {

if (!r.bonusIneligible) return true;

// bonusIneligible fires only when bonusPer$ > 0 and currencies mismatch across airlines

return false;

});

// Sort: best net value first

rows.sort((a, b) => b.netValue - a.netValue);

const best = rows[0];


// Hero

document.getElementById('m-points').textContent = fmtPts(best.totalPts);

const curLabel = CURRENCY_LABEL[state.currency];

let bonusSub = '';

if (best.platformBonusPts) {

const src = best.bonusConverted ? ` (from ${best.rawBonusPts.toLocaleString()} ${best.bonusCurrency==='b2b'?'B2B':CURRENCY_SHORT[best.bonusCurrency]} pts)` : '';

bonusSub = ` + ${fmtPts(best.platformBonusPts)} bonus${src}`;

}

const bestLabel = best.tier && best.tier !== 'Standard' ? `${best.name} · ${best.tier}` : best.name;

document.getElementById('m-points-sub').textContent = `${curLabel} · via ${bestLabel} · card ${fmtPts(best.cardPts)}${bonusSub}${state.bonus?` + ${fmtPts(state.bonus)} promo`:''}`;


document.getElementById('m-cpp').textContent = isFinite(best.cpp) ? fmtCPP(best.cpp) : '—';

document.getElementById('m-cpp-sub').textContent = `cost per point (fee ${fmt$2(best.feeDollars)} + card fee ${fmt$(state.fee)})`;


const netEl = document.getElementById('m-net');

netEl.textContent = fmt$2(best.netValue);

document.querySelector('#hero .metric:nth-child(3)').classList.toggle('pos', best.netValue >= 0);

document.querySelector('#hero .metric:nth-child(3)').classList.toggle('neg', best.netValue < 0);

document.getElementById('m-net-sub').textContent = `at ${state.ppv}¢/pt (${fmt$(best.pointValue)} value − ${fmt$(best.totalCost)} cost)`;


// Table

const tbody = document.getElementById('tbody');

tbody.innerHTML = '';

rows.forEach((r, i) => {

const tr = document.createElement('tr');

if (i === 0) tr.classList.add('optimal');

// Bonus breakdown text

let bonusCell = '—';

if (r.platformBonusPts > 0) {

const label = r.bonusConverted

? `${fmtPts(r.platformBonusPts)} <span style="color:var(--ink-3);font-size:11px">(${fmtPts(r.rawBonusPts)} B2B ${CURRENCY_SHORT[state.currency]})</span>`

: `${fmtPts(r.platformBonusPts)}`;

const inner = `<span style="color:var(--accent);font-weight:500">+${label}</span>`;

bonusCell = r.bonusSourceUrl

? `<a href="${r.bonusSourceUrl}" target="_blank" rel="noopener" title="${r.bonusSourceLabel||'View bonus source'}" style="color:inherit;text-decoration:none;border-bottom:1px dotted var(--line-2);cursor:help">${inner}<span style="font-size:9px;color:var(--ink-3);margin-left:3px;vertical-align:1px"></span></a>`

: inner;

} else if (r.bonusIneligible) {

bonusCell = `<span style="color:var(--ink-3);font-size:11px">${CURRENCY_SHORT[r.bonusCurrency]||'B2B'}-only, no ${CURRENCY_SHORT[state.currency]} transfer</span>`;

}

// Platform name: show tier if non-Standard

const tierLabel = r.tier && r.tier !== 'Standard'

? `<span style="color:var(--accent);font-size:11px;font-weight:500;background:rgba(184,83,46,0.08);padding:1px 6px;border-radius:3px;margin-left:6px">${r.tier}</span>` : '';

tr.innerHTML = `

<td class="name"><a href="${PLATFORM_LINKS[r.name]||'#'}" target="_blank" rel="sponsored noopener" data-platform="${r.name}" data-tier="${r.tier||'Standard'}" style="color:inherit;text-decoration:none;border-bottom:1px dotted var(--line-2)" onclick="trackReferral('${r.name}','${r.tier||'Standard'}')">${r.name}</a>${tierLabel}<div style="font-family:var(--mono);font-size:11px;color:var(--ink-3);margin-top:2px;font-weight:400">${r.note}</div></td>

<td class="num">${fmtPct(r.feePct)}</td>

<td class="num">${fmt$2(r.feeDollars)}</td>

<td class="num">${fmtPts(r.cardPts)}</td>

<td class="num">${bonusCell}</td>

<td class="num" style="font-weight:600">${fmtPts(r.totalPts)} <span style="color:var(--ink-3);font-size:11px;font-weight:400">${CURRENCY_SHORT[state.currency]}</span></td>

<td class="num">${isFinite(r.cpp)?fmtCPP(r.cpp):'—'}</td>

<td class="num" style="color:${r.netValue>=0?'var(--positive)':'var(--negative)'};font-weight:500">${fmt$2(r.netValue)}</td>

`;

tbody.appendChild(tr);

});


// Update table header to reflect currency

document.getElementById('th-total').textContent = CURRENCY_SHORT[state.currency] + ' total';


// Break-even

// Break-even fee % = value the card earns on $1 spend / (1 + card earn value on fee) ≈ points_per_dollar × ppv/100

// Since card earns on both bill and fee, break-even (net = 0, no annual fee, no bonus) simplifies to:

// fee_pct such that (bill+fee) * earn * ppv/100 = fee

// let f = fee_pct, then (1+f)*earn*ppv/100 = f f = (earn*ppv/100) / (1 - earn*ppv/100)

const e = state.earn * state.ppv / 100;

const beFee = e < 1 ? e / (1 - e) : Infinity;

document.getElementById('be-pct').textContent = isFinite(beFee) ? fmtPct(beFee, 2) : '∞';

document.getElementById('be-best').textContent = fmtPct(best.feePct, 2) + ` (${bestLabel})`;

const headroom = beFee - best.feePct;

document.getElementById('be-head').textContent = (headroom>=0?'+':'') + fmtPct(headroom, 2);


const verdict = document.getElementById('verdict');

if (best.netValue > 0) {

verdict.className = 'verdict pos';

verdict.innerHTML = `<strong>Worth it.</strong> ${bestLabel} nets ${fmt$2(best.netValue)} of value on $${state.spend.toLocaleString()} spend. Your card earns points worth more than the platform fee.`;

} else if (best.netValue > -50) {

verdict.className = 'verdict warn';

verdict.innerHTML = `<strong>Marginal.</strong> ${bestLabel} is your best option but nets only ${fmt$2(best.netValue)}. Reconsider your point value assumption or wait for a signup bonus.`;

} else {

verdict.className = 'verdict neg';

verdict.innerHTML = `<strong>Not worth it.</strong> Even ${bestLabel} loses ${fmt$2(-best.netValue)} at ${state.ppv}¢/pt. Either you value points too low, your card's earn rate is too low for the ${state.network==='amex'?'Amex':'Visa/MC'} fee, or you should pay direct.`;

}


drawChart(rows.slice().sort((a,b)=>PLATFORMS.findIndex(p=>p.name===a.name)-PLATFORMS.findIndex(p=>p.name===b.name)));

}


// ---- Chart ----

// Chart shows only the *best-net-value* row per platform brand so it stays readable.

let chart;

function drawChart(currentRows) {

const spends = [1000, 2500, 5000, 10000, 25000, 50000, 100000];

// Bucket platforms by brand, pick each brand's best-performing tier at current spend

const brands = ['Sniip', 'B2Bpay', 'RewardPay', 'pay.com.au', 'Payment Logic'];

const bestPerBrand = brands.map(brand => {

const tiers = PLATFORMS.filter(p => p.name === brand);

let best = null; let bestNet = -Infinity;

tiers.forEach(t => {

const r = calcForPlatform(t);

if (r.netValue > bestNet) { bestNet = r.netValue; best = { platform: t, tier: t.tier }; }

});

return best;

});


const datasets = bestPerBrand.map((b, i) => {

const color = ['#b8532e', '#1c4d8f', '#6b4a91', '#2f6b3d', '#b0862a'][i];

const data = spends.map(s => {

const saved = state.spend;

state.spend = s;

// Re-pick best tier at this spend level for accuracy

const tiers = PLATFORMS.filter(p => p.name === b.platform.name);

let bestNet = -Infinity;

tiers.forEach(t => {

const r = calcForPlatform(t);

if (r.netValue > bestNet) bestNet = r.netValue;

});

state.spend = saved;

return bestNet;

});

return {

label: b.platform.name,

data,

borderColor: color,

backgroundColor: color + '20',

borderWidth: b.platform.name === currentRows[0].name ? 2.5 : 1.5,

tension: 0.25,

pointRadius: 3,

pointHoverRadius: 5,

};

});


if (chart) chart.destroy();

const ctx = document.getElementById('chart').getContext('2d');

chart = new Chart(ctx, {

type: 'line',

data: { labels: spends.map(s => '$' + (s>=1000?s/1000+'k':s)), datasets },

options: {

responsive: true, maintainAspectRatio: false,

plugins: {

legend: { position: 'bottom', labels: { font: { family: 'General Sans', size: 12 }, boxWidth: 12, boxHeight: 12, padding: 12 } },

tooltip: {

backgroundColor: '#1a1a1a', titleFont: { family: 'General Sans', weight: '600' },

bodyFont: { family: 'JetBrains Mono' }, padding: 10, cornerRadius: 4,

callbacks: { label: (c) => `${c.dataset.label}: ${fmt$2(c.raw)}` }

}

},

scales: {

y: {

grid: { color: '#ecebe4' }, border: { display: false },

ticks: { font: { family: 'JetBrains Mono', size: 11 }, color: '#86867f', callback: (v) => fmt$(v) }

},

x: {

grid: { display: false }, border: { color: '#dedcd2' },

ticks: { font: { family: 'JetBrains Mono', size: 11 }, color: '#86867f' }

}

}

}

});

}


// ---- Bind inputs ----

document.querySelectorAll('#network-seg button').forEach(b => {

b.addEventListener('click', () => {

document.querySelectorAll('#network-seg button').forEach(x => x.classList.remove('active'));

b.classList.add('active');

state.network = b.dataset.net;

render();

});

});


['spend','earn','bonus','ppv','fee'].forEach(id => {

document.getElementById(id).addEventListener('input', (e) => {

const v = parseFloat(e.target.value);

state[id] = isNaN(v) ? 0 : v;

render();

});

});


document.querySelectorAll('#billtype-seg button').forEach(b => {

b.addEventListener('click', () => {

document.querySelectorAll('#billtype-seg button').forEach(x => x.classList.remove('active'));

b.classList.add('active');

state.billType = b.dataset.bt;

render();

});

});


document.querySelectorAll('#currency-seg button').forEach(b => {

b.addEventListener('click', () => {

document.querySelectorAll('#currency-seg button').forEach(x => x.classList.remove('active'));

b.classList.add('active');

state.currency = b.dataset.cur;

render();

});

});


document.querySelectorAll('.preset').forEach(p => {

p.addEventListener('click', () => {

if (p.dataset.spend) { state.spend = +p.dataset.spend; document.getElementById('spend').value = state.spend; }

if (p.dataset.earn) { state.earn = +p.dataset.earn; document.getElementById('earn').value = state.earn; }

if (p.dataset.ppv) { state.ppv = +p.dataset.ppv; document.getElementById('ppv').value = state.ppv; }

render();

});

});


render();

</script>

</body>

</html>