Test record
Describe an app in plain English. AI builds one that works.
✓ Built & tested by us
Date14-15 Oct 2026
Tool and tierThe bill splitter was written by our AI agent (Grok Bot) from the prompt below. It has NOT yet been rebuilt in Claude, ChatGPT or Gemini (free tier), so the consumer-app result is unverified.
Test dataFictional or public-domain only
Exact prompt
Build me a simple web app I can open on my phone: a bill splitter for 5 friends after dinner.
- I can type the 5 names and how much each person actually paid.
- I can add the items we ordered with prices, and tick who shared each item (default: everyone).
- I can add tip % and tax %, split proportionally to what each person ate.
- Show what each person owes and the fewest payments needed to settle up ("Anna pays Ben €12.30").
- Handle cents correctly so the totals add up exactly.
- Everything stays on my phone: no login, no saving data online.
Make it a single page that works when I open it.Sample input
- The exact prompt · download prompt.txt
Build me a simple web app I can open on my phone: a bill splitter for 5 friends after dinner. - I can type the 5 names and how much each person actually paid. - I can add the items we ordered with prices, and tick who shared each item (default: everyone). - I can add tip % and tax %, split proportionally to what each person ate. - Show what each person owes and the fewest payments needed to settle up ("Anna pays Ben €12.30"). - Handle cents correctly so the totals add up exactly. - Everything stays on my phone: no login, no saving data online. Make it a single page that works when I open it.
Raw output
- Output of the runnable checks, 15 Oct 2026 · download test-run-output.txt
default scenario {"subtotal":74,"total":81.4,"owed":{"Anna":17.38,"Ben":15.55,"Chiara":9.68,"Dev":15.54,"Ema":23.25},"transfers":[{"from":"Ema","to":"Ben","amount":23.25},{"from":"Anna","to":"Ben","amount":17.38},{"from":"Chiara","to":"Ben","amount":5.22},{"from":"Chiara","to":"Dev","amount":4.46}],"paidMismatch":0} ALL 1808 CHECKS PASSED (fixed cases, default dinner, 400 random settle-up cases checked against an exhaustive minimum-payments search, 500 random split cases)
What went wrong, and limits
1) The first version (14 Oct) settled debts with a greedy method: totals were always right but the number of payments was not guaranteed to be the fewest. 2) After an outside review on 15 Oct we replaced it with an exact bitmask search and added a check against an independent exhaustive search. 3) Limits: one currency; tax and tip are split in proportion to what each person ate; no rounding options; no saving.
Runnable test code
Runnable checks. Run: node scripts/test_splitter.js (scripts/test_splitter.js)
// Runnable checks for the bill splitter: `node scripts/test_splitter.js`
const {computeSplit,settle}=require('../site/public/apps/bill-splitter/splitter.js');const a0=require('assert');
let checks=0;const a=new Proxy(a0,{get:(t,k)=>(...args)=>{checks++;return t[k](...args)}});
// independent reference: minimum number of transactions by exhaustive backtracking
function minTx(arr){arr=arr.filter(x=>x!==0);let best=Infinity;(function dfs(i,c){while(i<arr.length&&arr[i]===0)i++;if(i===arr.length){best=Math.min(best,c);return}if(c>=best)return;const seen=new Set();for(let j=i+1;j<arr.length;j++){if(arr[i]*arr[j]<0&&!seen.has(arr[j])){seen.add(arr[j]);arr[j]+=arr[i];dfs(i+1,c+1);arr[j]-=arr[i]}}})(0,0);return best}
let r=computeSplit(['A','B','C'],[{price:10}],0,0,{A:10});a.deepStrictEqual(r.owed,{A:3.34,B:3.33,C:3.33});a.strictEqual(r.transfers.length,2);
r=computeSplit(['A','B'],[{price:20,sharedBy:['A']},{price:10}],10,0,{B:33});a.strictEqual(r.total,33);a.strictEqual(r.owed.A+r.owed.B,33);a.deepStrictEqual(r.transfers,[{from:'A',to:'B',amount:r.owed.A}]);
const P=['Anna','Ben','Chiara','Dev','Ema'];r=computeSplit(P,[{price:9},{price:11},{price:8,sharedBy:['Anna','Chiara']},{price:28,sharedBy:['Ben','Dev','Ema']},{price:14,sharedBy:['Anna','Ema']},{price:4}],0,10,{Ben:61.4,Dev:20});
const sum=Object.values(r.owed).reduce((x,y)=>x+y,0);a.ok(Math.abs(sum-r.total)<1e-9);console.log('default scenario',JSON.stringify(r));
// settle is optimal on a case where greedy needs 3 payments but 2 suffice: balances +5,-5 and +3,-3 hidden among 4 people
const t=settle([['a',500],['b',-300],['c',-200],['d',0]]);a.strictEqual(t.length,2);
const t2=settle([['a',500],['b',300],['c',-500],['d',-300]]);a.strictEqual(t2.length,2); // two independent pairs
for(let k=0;k<400;k++){const n=2+k%6,bal=[];let s=0;for(let i=0;i<n-1;i++){const v=Math.round((Math.random()-.5)*4000);bal.push(['p'+i,v]);s+=v}bal.push(['p'+(n-1),-s]);
const tr=settle(bal);const net=Object.fromEntries(bal.map(b=>[b[0],b[1]]));tr.forEach(x=>{net[x.from]+=Math.round(x.amount*100);net[x.to]-=Math.round(x.amount*100)});
a.ok(Object.values(net).every(v=>v===0),'balances not settled');a.strictEqual(tr.length,minTx(bal.map(b=>b[1])),'not minimal')}
for(let t=0;t<500;t++){const n=2+t%4,ps=P.slice(0,n),its=[...Array(1+t%5)].map((_,k)=>({price:Math.round(Math.random()*5000)/100,sharedBy:k%2?[ps[k%n]]:undefined}));
const pay={};ps.forEach((p,i)=>pay[p]=i===0?Math.round(its.reduce((x,y)=>x+y.price,0)*100*(1+(t%7+t%15)/100))/100:0);
const rr=computeSplit(ps,its,t%7,t%15,pay);const s=Object.values(rr.owed).reduce((x,y)=>x+y,0);a.ok(Math.abs(s-rr.total)<1e-6,'sum mismatch');
const tr=rr.transfers.reduce((x,y)=>x+y.amount,0);a.ok(tr<=rr.total+1e-6)}
console.log('ALL '+checks+' CHECKS PASSED (fixed cases, default dinner, 400 random settle-up cases checked against an exhaustive minimum-payments search, 500 random split cases)');
The calculation code that the checks exercise (site/public/apps/bill-splitter/splitter.js)
// Bill splitter core (pure functions, testable in Node). Amounts handled in integer cents.
function computeSplit(people, items, taxPct, tipPct, payments){
const n=people.length, owed=Object.fromEntries(people.map(p=>[p,0]));
let subtotal=0;
for(const it of items){
const c=Math.round(it.price*100); subtotal+=c;
const sh=(it.sharedBy&&it.sharedBy.length)?it.sharedBy:people;
const base=Math.floor(c/sh.length); let rem=c-base*sh.length;
sh.forEach((p,i)=>{owed[p]+=base+(i<rem?1:0)});
}
const extra=Math.round(subtotal*(taxPct+tipPct)/100);
// distribute tax+tip proportionally, fix rounding on largest shares
let assigned=0; const shares=people.map(p=>{const s=subtotal?Math.floor(extra*owed[p]/subtotal):0;assigned+=s;return [p,s]});
shares.sort((a,b)=>owed[b[0]]-owed[a[0]]); for(let i=0;assigned<extra;i=(i+1)%n){shares[i][1]++;assigned++}
shares.forEach(([p,s])=>owed[p]+=s);
const total=subtotal+extra, paid=Object.fromEntries(people.map(p=>[p,Math.round((payments[p]||0)*100)]));
const bal=people.map(p=>[p,paid[p]-owed[p]]);
const transfers=settle(bal);
const paidTotal=Object.values(paid).reduce((a,b)=>a+b,0);
return {subtotal:subtotal/100,total:total/100,owed:Object.fromEntries(people.map(p=>[p,owed[p]/100])),transfers,paidMismatch:(paidTotal-total)/100};
}
// Fewest possible payments. Balances are integer cents (positive = is owed money).
// Exact for up to 16 people with non-zero balances (bitmask search); otherwise falls back to a greedy match.
function matchGroup(group){
const cred=group.filter(b=>b[1]>0).map(b=>[b[0],b[1]]).sort((a,b)=>b[1]-a[1]), deb=group.filter(b=>b[1]<0).map(b=>[b[0],-b[1]]).sort((a,b)=>b[1]-a[1]);
const out=[]; let i=0,j=0;
while(i<deb.length&&j<cred.length){const x=Math.min(deb[i][1],cred[j][1]);out.push({from:deb[i][0],to:cred[j][0],amount:x/100});deb[i][1]-=x;cred[j][1]-=x;if(!deb[i][1])i++;if(!cred[j][1])j++}
return out;
}
function settle(bal){
const nz=bal.filter(b=>b[1]!==0), n=nz.length;
if(n===0)return [];
if(n>16)return matchGroup(nz);
const N=1<<n, sum=new Int32Array(N), dp=new Int8Array(N);
for(let m=1;m<N;m++){const low=m&-m, i=31-Math.clz32(low); sum[m]=sum[m^low]+nz[i][1]}
for(let m=1;m<N;m++){let best=0;for(let i=0;i<n;i++)if(m&(1<<i)){const v=dp[m^(1<<i)];if(v>best)best=v}dp[m]=best+(sum[m]===0?1:0)}
// reconstruct an order whose zero-sum prefixes give the maximum number of groups
const order=[];let m=N-1;
while(m){for(let i=0;i<n;i++)if(m&(1<<i)&&dp[m^(1<<i)]+(sum[m]===0?1:0)===dp[m]){order.push(i);m^=(1<<i);break}}
order.reverse();
const groups=[];let cur=[],acc=0;
for(const i of order){cur.push(nz[i]);acc+=nz[i][1];if(acc===0){groups.push(cur);cur=[]}}
const transfers=[];for(const g of groups)transfers.push(...matchGroup(g));
return transfers;
}
if(typeof module!=='undefined')module.exports={computeSplit,settle};
Full working log
# describe-an-app: evidence Status: BUILT & TESTED HERE (by the project's AI agent, Grok Bot). Consumer re-build: PENDING. - 2026-10-14. Prompt: scripts/app_prompt.txt. Output: site/public/apps/bill-splitter/ (index.html + splitter.js), embedded on homepage. - Tests: scripts/test_splitter.js on Node 20.19.2: 2 hand-checked cases, default 5-friend scenario (total €81.40; Anna €17.38 hand-verified: 1.80+2.20+4.00+7.00+0.80=15.80 +10%), 500 randomized cases: per-person shares always sum to the total to the cent. ALL PASSED. - Mobile check: headless Chrome 390px screenshot, result visible above fold. - Limits: settle-up is greedy (minimal in typical cases, not guaranteed optimal); tip/tax split proportionally; one currency. ## Consumer test script (pending) Inputs: inputs/prompt.txt. Run in each of: Claude (Artifacts), ChatGPT (Canvas), Gemini (Canvas), free tier. 1. New chat, paste prompt verbatim. 2. Open preview. 3. Enter the default scenario (names Anna..Ema; paid Ben 61.40, Dev 20; items 9, 11, 8 [Anna,Chiara], 28 [Ben,Dev,Ema], 14 [Anna,Ema], 4; tip 10%). Pass: totals €81.40, Anna €17.38, Ema €23.25 (±€0.01) and a working settle-up. Record: works first try? bugs? number of follow-up prompts needed; screenshot.