|
<!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 & 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&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 & 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 & 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 & fees</a></li> |
|
<li>RewardPay — <a href="https://www.rewardpay.com.au/pricing" target="_blank" rel="noopener">pricing & 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&Cs</a></li> |
|
<li>B2Bpay Earn Plus (1 B2B pt / $2) & 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 & 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 & 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 QF↔QF, VF↔VF, B2B→QF, B2B→VF. Cross-airline (QF↔VF) 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> |
|
|