Bookkeeping Services Calculator

Give your bookkeeping or accounting firm a quote tool that prices the engagement the way you do. The owner picks the entity, the industry, the transactions a month, the accounts, the revenue and the software, and the calculator works out the rest in front of them: which return the entity files (Schedule C, 1120-S, 1065, 1120 or a 990 sized by receipts), that an S corp owner has to be on payroll while a partner cannot be, which K-1s follow, whether sales tax applies and whether an online seller is past the economic nexus threshold. The result is three numbers instead of one: every month by transaction tier with accrual, quarterly close, dedicated bookkeeper, payroll by the head and add-ons; once at the start with onboarding, setup or migration and catch-up months; and once a year with the return, quarterly estimates, W-2s and 1099s. The thresholds the law moves are named constants you keep current yourself.

B2B Services

Try the Calculator

Bookkeeping, Payroll & Tax Estimate
You do not need to know which return you file or whether sales tax applies - we work that out as you go. Beside the questions you will see the monthly fee, the one-time setup and what comes once a year. We reply within 24 hours.
🏢 Your Business
 
 
📒 Your Books
120a month
120 a month
101500
 
$
 
 
Tier 2: up to 250 transactions a month. Every plan categorizes the transactions, reconciles 2 accounts and closes the month with a P&L and a balance sheet.
 
 
👥 Payroll
 
 
You are not on payroll: a sole proprietor takes owner's draws, never a W-2 paycheck. The tax comes through quarterly estimated payments instead.
🧾 Taxes
🤝 Working Together
 
 
 
 
 

💰 Your Estimate
$ 449
/mo
+
$ 90
/mo

$ 539
per month, flat
$ 250
one-time
$ 850
per year
$485/mo when twelve months are paid up front (10% off).
🧾 Summary
$ 539
/month
$ 250
$ 850
Estimate only. The engagement letter after the call sets the final fee and scope.
📋 Your Contact Details
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
export function bookkeepingServicesCalculator(form: FormTs) {
// Monthly bookkeeping tiers by transactions a month (lines on the bank and card statements)
const tiers = [
{ max: 100, monthly: 299 },
{ max: 250, monthly: 449 },
{ max: 500, monthly: 649 },
{ max: 1000, monthly: 949 }
];
const OVER_TOP_PER_100 = 75; // each started hundred transactions above the top tier
const ACCOUNTS_INCLUDED = 2; // bank and credit card accounts every plan reconciles
const ACCOUNT_MONTHLY = 35; // each additional account
const ACCRUAL_PCT = 25; // added for accrual-basis books
const QUARTERLY_OFF_PCT = 20; // taken off for a quarterly close instead of monthly
const DEDICATED_PCT = 20; // added for a dedicated bookkeeper instead of the pooled team
const RECEIPTS_APP_MONTHLY = 25; // the receipt capture app
 
// Monthly add-ons
const addonPrices: Record<string, number> = {
'ar': 100,
'ap': 125,
'inventory': 90,
'jobCosting': 110
};
const addonNames: Record<string, string> = {
'ar': 'Invoicing and A/R',
'ap': 'Bill pay (A/P)',
'inventory': 'Inventory tracking',
'jobCosting': 'Job costing and class tracking'
};
const CFO_HOURLY = 175;
 
// Payroll
const payrollPrices = {
baseMonthly: 60,
employeeMonthly: 12,
contractorMonthly: 6,
weeklyPct: 25, // added for weekly pay runs
w2Each: 15, // per W-2 at year-end
n1099Each: 20 // per 1099-NEC at year-end
};
 
// Sales tax filings, per state per month
const SALES_TAX_STATE_MONTHLY = 45;
 
// Annual tax returns (federal and one state)
const returnPrices = {
scheduleC: 550,
s1120: 1200,
p1065: 1200,
c1120: 1500,
n990n: 150,
n990ez: 750,
n990: 1800,
k1Each: 75, // each owner beyond the first on an 1120-S or 1065
estimatesYear: 300 // quarterly estimated taxes, per year
};
 
// One-time
const oneTimePrices = {
onboarding: 250,
newFileSetup: 400,
migration: 600,
catchupPct: 120 // one month of catch-up as % of the monthly bookkeeping fee
};
 
// Thresholds the law sets
const NEXUS_THRESHOLD = 100000; // economic nexus for remote sellers, most states
const N1099_THRESHOLD = 2000; // 1099-NEC reporting amount
const N990_POSTCARD_MAX = 50000; // gross receipts up to which a nonprofit files the 990-N
const N990_EZ_MAX = 200000; // gross receipts up to which a nonprofit may file the 990-EZ
 
const ANNUAL_PREPAY_PCT = 10; // cheaper per month when twelve months are paid up front
const PROMO_PCT = 0; // discount on the monthly fee; 0 hides the banner
const PROMO_TEXT = 'New client special';
const REPLY_HOURS = 24;
const CONSULT_MINUTES = 30;
 
// Entities: the return they file, whether a K-1 goes to each owner, and whether the owner is on payroll
type EntityId = 'sole' | 'scorp' | 'partnership' | 'ccorp' | 'nonprofit';
const entityNames: Record<EntityId, string> = {
'sole': 'Sole proprietor or single-member LLC',
'scorp': 'S corporation',
'partnership': 'Partnership or multi-member LLC',
'ccorp': 'C corporation',
'nonprofit': 'Nonprofit'
};
const entityK1: Record<EntityId, boolean> = { 'sole': false, 'scorp': true, 'partnership': true, 'ccorp': false, 'nonprofit': false };
const entityPassThrough: Record<EntityId, boolean> = { 'sole': true, 'scorp': true, 'partnership': true, 'ccorp': false, 'nonprofit': false };
const entityOwnerPayroll: Record<EntityId, 'required' | 'optional' | 'none'> = { 'sole': 'none', 'scorp': 'required', 'partnership': 'none', 'ccorp': 'optional', 'nonprofit': 'none' };
 
// Industries: whether sales tax is part of the job, and whether sales cross state lines
type IndustryId = 'contractor' | 'restaurant' | 'ecommerce' | 'professional' | 'realestate' | 'other';
const industryNames: Record<IndustryId, string> = {
'contractor': 'Contractor and trades',
'restaurant': 'Restaurant and food',
'ecommerce': 'E-commerce',
'professional': 'Professional services',
'realestate': 'Real estate',
'other': 'Something else'
};
const industrySalesTax: Record<IndustryId, 'yes' | 'no' | 'maybe'> = { 'contractor': 'maybe', 'restaurant': 'yes', 'ecommerce': 'yes', 'professional': 'no', 'realestate': 'no', 'other': 'maybe' };
 
type SoftwareStatusId = 'have' | 'new' | 'migrate';
const softwareStatusNames: Record<SoftwareStatusId, string> = { 'have': 'Already set up', 'new': 'Needs setup', 'migrate': 'Migrating' };
const softwareStatusFee: Record<SoftwareStatusId, number> = { 'have': 0, 'new': oneTimePrices.newFileSetup, 'migrate': oneTimePrices.migration };
 
const money = (value: number) => (value < 0 ? '-$' : '$') + Math.round(Math.abs(value)).toLocaleString('en-US');
const count = (value: number) => Math.round(value).toLocaleString('en-US');
const plural = (value: number, one: string, many: string) => value + ' ' + (value === 1 ? one : many);
 
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Bookkeeping, Payroll & Tax Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
 
form.addRow(row => {
row.addTextPanel('intro', {
computedValue: () => 'You do not need to know which return you file or whether sales tax applies - we work that out as you go. Beside the questions you will see the monthly fee, the one-time setup and what comes once a year. We reply within ' + REPLY_HOURS + ' hours.',
customStyles: { 'font-size': '0.95rem', 'color': '#475569' }
});
});
 
form.addSpacer({ height: 20 });
 
// Business Section
const businessSection = form.addSubform('business', { title: '🏢 Your Business' });
 
businessSection.addRow(row => {
row.addRadioButton('entity', {
label: 'How is the business set up?',
options: [
{ id: 'sole', name: 'Sole proprietor or single-member LLC - Schedule C on your personal return' },
{ id: 'scorp', name: 'S corporation - Form 1120-S, a K-1 per shareholder' },
{ id: 'partnership', name: 'Partnership or multi-member LLC - Form 1065, a K-1 per partner' },
{ id: 'ccorp', name: 'C corporation - Form 1120, pays its own tax' },
{ id: 'nonprofit', name: 'Nonprofit - Form 990, the version depends on size' }
],
defaultValue: 'sole',
orientation: 'vertical',
tooltip: 'Not sure? Pick what your bank paperwork says; we confirm it on the call',
isRequired: true
});
});
 
const getEntity = (): EntityId => {
const value = businessSection.radioButton('entity')?.value();
return value === 'scorp' || value === 'partnership' || value === 'ccorp' || value === 'nonprofit' ? value : 'sole';
};
const isK1Entity = () => entityK1[getEntity()];
 
businessSection.addRow(row => {
row.addInteger('owners', {
label: () => getEntity() === 'scorp' ? 'Shareholders' : 'Partners or members',
min: 1,
max: 20,
defaultValue: 1,
tooltip: 'Each one gets a K-1 with the annual return',
isVisible: () => isK1Entity()
}, '1fr');
row.addRadioButton('situation', {
label: 'Where are the books today?',
options: [
{ id: 'starting', name: 'Just starting out - no books yet' },
{ id: 'switching', name: 'Switching bookkeepers' },
{ id: 'diy', name: 'Doing it myself - spreadsheets or my own software' }
],
defaultValue: 'diy',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
 
businessSection.addRow(row => {
row.addDropdown('industry', {
label: 'What line of business?',
options: [
{ id: 'contractor', name: 'Contractor and trades' },
{ id: 'restaurant', name: 'Restaurant and food' },
{ id: 'ecommerce', name: 'E-commerce' },
{ id: 'professional', name: 'Professional services' },
{ id: 'realestate', name: 'Real estate' },
{ id: 'other', name: 'Something else' }
],
defaultValue: 'other',
tooltip: 'The industry tells us what to expect in the books, and whether sales tax is part of the job',
isRequired: true
}, '1fr');
});
 
const getOwners = () => isK1Entity() ? (businessSection.integer('owners')?.value() || 1) : 1;
const getSituation = () => businessSection.radioButton('situation')?.value() || 'diy';
const isStartingOut = () => getSituation() === 'starting';
const getIndustry = (): IndustryId => {
const value = businessSection.dropdown('industry')?.value();
return value === 'contractor' || value === 'restaurant' || value === 'ecommerce' || value === 'professional' || value === 'realestate' ? value : 'other';
};
 
// Books Section
const booksSection = form.addSubform('books', { title: '📒 Your Books' });
 
booksSection.addRow(row => {
row.addSlider('transactions', {
label: 'Transactions a month',
min: 10,
max: 1500,
step: 10,
defaultValue: 120,
unit: 'a month',
tooltip: 'Every line on your bank and credit card statements: deposits, card charges, checks, transfers. A ballpark is fine',
isRequired: true
});
});
 
booksSection.addRow(row => {
row.addInteger('accounts', {
label: 'Bank and credit card accounts (' + ACCOUNTS_INCLUDED + ' included)',
min: 1,
max: 30,
defaultValue: 2,
tooltip: 'Checking, savings, credit cards, loans, merchant accounts',
isRequired: true
}, '1fr');
row.addMoney('revenue', {
label: 'Annual revenue (ballpark)',
currency: '$',
min: 0,
max: 50000000,
step: 5000,
defaultValue: 350000,
tooltip: 'It decides sales tax across states and, for a nonprofit, which 990 you file',
isRequired: true
}, '1fr');
});
 
const getTransactions = () => booksSection.slider('transactions')?.value() || 120;
const getAccounts = () => booksSection.integer('accounts')?.value() || 1;
const getRevenue = () => booksSection.money('revenue')?.value() || 0;
 
// The plan tier the volume lands in, and what the volume past the top tier adds
const getTierIndex = () => {
for (let i = 0; i < tiers.length; i++) {
const tier = tiers[i];
if (tier && getTransactions() <= tier.max) return i;
}
return tiers.length - 1;
};
const getTierNumber = () => getTierIndex() + 1;
const getTierMax = () => tiers[getTierIndex()]?.max || 0;
const getTierMonthly = () => tiers[getTierIndex()]?.monthly || 0;
const getTopTierMax = () => tiers[tiers.length - 1]?.max || 0;
const getOverTop = () => Math.max(Math.ceil((getTransactions() - getTopTierMax()) / 100), 0);
 
booksSection.addRow(row => {
row.addRadioButton('basis', {
label: 'Accounting basis',
options: [
{ id: 'cash', name: 'Cash basis' },
{ id: 'accrual', name: 'Accrual basis (+' + ACCRUAL_PCT + '%)' }
],
defaultValue: 'cash',
tooltip: 'Most small businesses run on cash. Accrual matters when you carry inventory, invoice on terms or a lender asks for GAAP statements'
});
});
 
const isAccrual = () => booksSection.radioButton('basis')?.value() === 'accrual';
 
booksSection.addRow(row => {
row.addTextPanel('tierInfo', {
computedValue: () => 'Tier ' + getTierNumber() + ': up to ' + count(getTierMax()) + ' transactions a month. '
+ (getOverTop() > 0
? count(getTransactions()) + ' a month is past our largest tier, so every started hundred above ' + count(getTopTierMax()) + ' is priced on top at $' + OVER_TOP_PER_100 + '.'
: 'Every plan categorizes the transactions, reconciles ' + ACCOUNTS_INCLUDED + ' accounts and closes the month with a P&L and a balance sheet.')
+ (getAccounts() > ACCOUNTS_INCLUDED ? ' Your ' + getAccounts() + ' accounts mean ' + (getAccounts() - ACCOUNTS_INCLUDED) + ' more to reconcile, $' + ACCOUNT_MONTHLY + ' a month each.' : ''),
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
 
booksSection.addRow(row => {
row.addDropdown('software', {
label: 'Which software?',
options: [
{ id: 'qbo', name: 'QuickBooks Online' },
{ id: 'xero', name: 'Xero' },
{ id: 'wave', name: 'Wave' }
],
defaultValue: 'qbo',
tooltip: 'The subscription stays in your name; we work in your file',
isRequired: true
}, '1fr');
row.addRadioButton('softwareStatus', {
label: 'Where does that file stand?',
options: [
{ id: 'have', name: 'Already set up - you have a file we take over' },
{ id: 'new', name: 'Needs setup - a new file and chart of accounts (+$' + oneTimePrices.newFileSetup + ' once)' },
{ id: 'migrate', name: 'Migrating from another system or spreadsheets (+$' + oneTimePrices.migration + ' once)' }
],
defaultValue: 'have',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
 
booksSection.addRow(row => {
row.addInteger('monthsBehind', {
label: 'Months behind (each ' + oneTimePrices.catchupPct + '% of the monthly bookkeeping fee)',
min: 0,
max: 36,
defaultValue: 0,
tooltip: 'Months nobody has booked yet. Zero if the books are current',
isVisible: () => !isStartingOut()
}, '1fr');
});
 
const softwareNames: Record<string, string> = { 'qbo': 'QuickBooks Online', 'xero': 'Xero', 'wave': 'Wave' };
const getSoftware = () => softwareNames[booksSection.dropdown('software')?.value() || 'qbo'] || 'QuickBooks Online';
const getSoftwareStatus = (): SoftwareStatusId => {
const value = booksSection.radioButton('softwareStatus')?.value();
return value === 'new' || value === 'migrate' ? value : 'have';
};
const getMonthsBehind = () => isStartingOut() ? 0 : (booksSection.integer('monthsBehind')?.value() || 0);
 
booksSection.addRow(row => {
row.addTextPanel('catchupInfo', {
computedValue: () => 'Catch-up goes into the one-time cost. Bringing ' + plural(getMonthsBehind(), 'month', 'months') + ' current is work backwards: statements to pull, receipts to hunt down, and usually a late return or two to file.',
customStyles: { 'font-size': '0.9rem', 'color': '#b45309', 'background': '#fef3c7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => getMonthsBehind() > 0
});
});
 
// Payroll Section
const payrollSection = form.addSubform('payroll', { title: '👥 Payroll' });
 
payrollSection.addRow(row => {
row.addInteger('employees', {
label: 'W-2 employees ($' + payrollPrices.employeeMonthly + '/mo each)',
min: 0,
max: 500,
defaultValue: 0,
tooltip: 'Not counting yourself'
}, '1fr');
row.addInteger('contractors', {
label: '1099 contractors ($' + payrollPrices.contractorMonthly + '/mo each)',
min: 0,
max: 500,
defaultValue: 0,
tooltip: 'Subs, freelancers, anyone you pay without withholding'
}, '1fr');
});
 
const getEmployees = () => payrollSection.integer('employees')?.value() || 0;
const getContractors = () => payrollSection.integer('contractors')?.value() || 0;
const getOwnerPayrollRule = () => entityOwnerPayroll[getEntity()];
 
payrollSection.addRow(row => {
row.addCheckbox('ownerPayroll', {
label: 'Owner on payroll - you draw a salary with withholding, like an employee',
defaultValue: true,
isVisible: () => getOwnerPayrollRule() === 'optional'
}, '1fr');
row.addRadioButton('frequency', {
label: 'How often do you pay?',
options: [
{ id: 'weekly', name: 'Weekly (+' + payrollPrices.weeklyPct + '%)' },
{ id: 'biweekly', name: 'Every two weeks' },
{ id: 'semimonthly', name: 'Twice a month' },
{ id: 'monthly', name: 'Monthly' }
],
defaultValue: 'biweekly',
isVisible: () => hasPayroll()
}, '1fr');
});
 
// The owner draws a salary when the law requires it, or when a C corp owner chose to
const isOwnerOnPayroll = () => getOwnerPayrollRule() === 'required'
|| (getOwnerPayrollRule() === 'optional' && payrollSection.checkbox('ownerPayroll')?.value() === true);
const getPayrollHeads = () => getEmployees() + (isOwnerOnPayroll() ? 1 : 0);
const hasPayroll = () => getPayrollHeads() > 0;
const frequencyNames: Record<string, string> = { 'weekly': 'weekly', 'biweekly': 'every two weeks', 'semimonthly': 'twice a month', 'monthly': 'monthly' };
const getFrequency = () => payrollSection.radioButton('frequency')?.value() || 'biweekly';
const isWeekly = () => hasPayroll() && getFrequency() === 'weekly';
 
payrollSection.addRow(row => {
row.addTextPanel('payrollInfo', {
computedValue: () => {
if (getOwnerPayrollRule() === 'required') {
return 'As an S corporation, you are on payroll: the IRS requires an S corp to pay its working shareholders a reasonable salary through payroll before any distributions, so we have added you to the headcount.';
}
if (getEntity() === 'partnership') {
return 'Partners are not on payroll: partners and LLC members take draws or guaranteed payments, never a W-2 paycheck. The tax comes through quarterly estimated payments instead.';
}
if (getEntity() === 'sole') {
return 'You are not on payroll: a sole proprietor takes owner\'s draws, never a W-2 paycheck. The tax comes through quarterly estimated payments instead.';
}
if (getContractors() > 0) {
return plural(getContractors(), 'contractor means a 1099-NEC', 'contractors mean 1099-NECs') + ' in January: every contractor paid $' + N1099_THRESHOLD.toLocaleString('en-US') + ' or more in the year gets one by January 31. We collect the W-9s during the year.';
}
return 'No payroll to run: the estimate leaves payroll out. Add employees whenever you hire and the fee follows.';
},
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
 
// Taxes Section
const taxesSection = form.addSubform('taxes', { title: '🧾 Taxes' });
 
const getSalesTaxRule = () => industrySalesTax[getIndustry()];
 
taxesSection.addRow(row => {
row.addCheckbox('salesTax', {
label: () => 'We collect sales tax (+$' + SALES_TAX_STATE_MONTHLY + '/state/mo)' + (getSalesTaxRule() === 'no' ? ' - unusual in your line of work, but some states tax certain services' : ' - retail sales, materials sold to customers, taxable services'),
defaultValue: false,
isVisible: () => getSalesTaxRule() !== 'yes'
}, '1fr');
});
 
const collectsSalesTax = () => getSalesTaxRule() === 'yes' || taxesSection.checkbox('salesTax')?.value() === true;
const isOverNexus = () => getIndustry() === 'ecommerce' && getRevenue() > NEXUS_THRESHOLD;
 
taxesSection.addRow(row => {
row.addInteger('states', {
label: 'States you file sales tax in ($' + SALES_TAX_STATE_MONTHLY + '/mo each)',
min: 1,
max: 50,
defaultValue: 1,
tooltip: 'Your home state counts as one',
isVisible: () => collectsSalesTax()
}, '1fr');
});
 
const getStates = () => collectsSalesTax() ? (taxesSection.integer('states')?.value() || 1) : 0;
 
taxesSection.addRow(row => {
row.addTextPanel('salesTaxInfo', {
computedValue: () => {
if (isOverNexus()) {
return 'You probably owe sales tax in more than one state. With about ' + money(getRevenue()) + ' in online sales you are likely past the economic nexus threshold of ' + money(NEXUS_THRESHOLD) + ' that most states use for out-of-state sellers. Count the states you already know about above; we map the rest on the call.';
}
if (getIndustry() === 'restaurant') {
return 'Restaurants collect sales tax on every ticket: prepared food is taxable in every state that has a sales tax, so the filings are part of your plan rather than a choice.';
}
return 'Online sales are taxable sales: what you sell online is taxed where the buyer is. At minimum you file in your home state; the bookkeeper maps the rest with you on the call.';
},
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => getSalesTaxRule() === 'yes'
});
});
 
// The return the business files: from the entity, or for a nonprofit from its size
const getReturn = (): { fee: number; name: string; note: string } => {
const entity = getEntity();
if (entity === 'sole') return { fee: returnPrices.scheduleC, name: 'Form 1040 with Schedule C', note: 'federal and one state' };
if (entity === 'scorp') return { fee: returnPrices.s1120, name: 'Form 1120-S', note: 'with a K-1 for each of ' + plural(getOwners(), 'owner', 'owners') };
if (entity === 'partnership') return { fee: returnPrices.p1065, name: 'Form 1065', note: 'with a K-1 for each of ' + plural(getOwners(), 'owner', 'owners') };
if (entity === 'ccorp') return { fee: returnPrices.c1120, name: 'Form 1120', note: 'federal and one state' };
if (getRevenue() <= N990_POSTCARD_MAX) return { fee: returnPrices.n990n, name: 'Form 990-N', note: 'the e-Postcard, up to ' + money(N990_POSTCARD_MAX) + ' in receipts' };
if (getRevenue() <= N990_EZ_MAX) return { fee: returnPrices.n990ez, name: 'Form 990-EZ', note: 'the short return, up to ' + money(N990_EZ_MAX) + ' in receipts' };
return { fee: returnPrices.n990, name: 'Form 990', note: 'the full return, above ' + money(N990_EZ_MAX) + ' in receipts' };
};
const getReturnFeeIfTaken = () => getReturn().fee + Math.max(getOwners() - 1, 0) * returnPrices.k1Each;
 
taxesSection.addRow(row => {
row.addCheckbox('taxReturn', {
label: () => 'Annual return: ' + getReturn().name + ', ' + getReturn().note + ' (+' + money(getReturnFeeIfTaken()) + '/yr)',
defaultValue: true
}, '1fr');
row.addCheckbox('estimates', {
label: () => 'Quarterly estimated taxes - ' + (entityPassThrough[getEntity()] ? 'the safe-harbor math and the vouchers four times a year' : 'the corporation\'s own estimated payments, four times a year') + ' (+$' + returnPrices.estimatesYear + '/yr)',
defaultValue: true,
isVisible: () => getEntity() !== 'nonprofit'
}, '1fr');
});
 
const takesTaxReturn = () => taxesSection.checkbox('taxReturn')?.value() === true;
const takesEstimates = () => getEntity() !== 'nonprofit' && taxesSection.checkbox('estimates')?.value() === true;
 
taxesSection.addRow(row => {
row.addTextPanel('returnInfo', {
computedValue: () => getEntity() === 'nonprofit'
? 'Your size puts you on ' + getReturn().name + '. The IRS sizes the 990 by gross receipts: the 990-N e-Postcard up to ' + money(N990_POSTCARD_MAX) + ', the 990-EZ up to ' + money(N990_EZ_MAX) + ' (with assets under $500,000), the full 990 above that.'
: 'One return, ' + getOwners() + ' K-1s. A' + (getEntity() === 'scorp' ? 'n S corporation' : ' partnership') + ' pays no income tax itself: the ' + getReturn().name + ' hands each owner a K-1 and the tax lands on their personal returns. Each owner past the first is $' + returnPrices.k1Each + ' a year on the return.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => getEntity() === 'nonprofit' || (isK1Entity() && getOwners() > 1)
});
});
 
// Service Section
const serviceSection = form.addSubform('service', { title: '🤝 Working Together' });
 
serviceSection.addRow(row => {
row.addRadioButton('level', {
label: 'Who keeps your books?',
options: [
{ id: 'team', name: 'Bookkeeping team - a pooled team works your file; questions by e-mail and the client portal' },
{ id: 'dedicated', name: 'Dedicated bookkeeper - one person who knows your business and picks up the phone (+' + DEDICATED_PCT + '%)' }
],
defaultValue: 'dedicated',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('cadence', {
label: 'How often do you want statements?',
options: [
{ id: 'monthly', name: 'Monthly close - P&L and balance sheet by the 15th of the next month' },
{ id: 'quarterly', name: 'Quarterly close - statements four times a year (-' + QUARTERLY_OFF_PCT + '%)' }
],
defaultValue: 'monthly',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
 
serviceSection.addRow(row => {
row.addRadioButton('docs', {
label: 'How do receipts and statements reach us?',
options: [
{ id: 'app', name: 'Snap receipts in the app - we match them to the bank feed (+$' + RECEIPTS_APP_MONTHLY + '/mo)' },
{ id: 'feeds', name: 'Bank feeds and e-mail - your accounts sync on their own; you forward whatever else comes in' },
{ id: 'paper', name: 'Drop off the box - bring the receipts and statements once a month, we scan and file them' }
],
defaultValue: 'feeds',
orientation: 'vertical',
isRequired: true
});
});
 
serviceSection.addRow(row => {
row.addCheckboxList('addons', {
label: 'What else should we take off your plate?',
options: Object.keys(addonNames).map(id => ({ id, name: (addonNames[id] || id) + ' (+$' + (addonPrices[id] || 0) + '/mo)' })),
orientation: 'vertical',
tooltip: 'Pick only what you actually have - nothing is added just in case'
}, '1fr');
row.addInteger('cfoHours', {
label: 'CFO advisory hours a month ($' + CFO_HOURLY + '/hour)',
min: 0,
max: 40,
defaultValue: 0,
tooltip: 'Budgets, cash-flow forecasts and a review call with a senior accountant. Zero if the statements are enough'
}, '1fr');
});
 
serviceSection.addRow(row => {
row.addDropdown('timing', {
label: 'When would we start?',
options: [
{ id: 'asap', name: 'As soon as possible' },
{ id: 'nextMonth', name: 'Next month' },
{ id: 'newYear', name: 'January 1' },
{ id: 'comparing', name: 'Just comparing quotes' }
],
defaultValue: 'nextMonth',
isRequired: true
}, '1fr');
});
 
const isDedicated = () => serviceSection.radioButton('level')?.value() !== 'team';
const isQuarterly = () => serviceSection.radioButton('cadence')?.value() === 'quarterly';
const docsNames: Record<string, string> = { 'app': 'receipt capture app', 'feeds': 'bank feeds and e-mail', 'paper': 'paper drop-off' };
const getDocs = () => serviceSection.radioButton('docs')?.value() || 'feeds';
const usesApp = () => getDocs() === 'app';
const getAddons = () => (serviceSection.checkboxList('addons')?.value() || []).filter(id => id in addonNames);
const getCfoHours = () => serviceSection.integer('cfoHours')?.value() || 0;
const timingNames: Record<string, string> = { 'asap': 'as soon as possible', 'nextMonth': 'from next month', 'newYear': 'from January 1', 'comparing': 'just comparing quotes' };
const getTiming = () => timingNames[serviceSection.dropdown('timing')?.value() || 'nextMonth'] || 'from next month';
 
form.addSpacer({ height: 20, showLine: true, lineStyle: 'dashed' });
 
// Pricing helpers: every month, once at the start, once a year
const getBooksFee = () => {
const tierBase = getTierMonthly() + getOverTop() * OVER_TOP_PER_100;
const extraAccounts = Math.max(getAccounts() - ACCOUNTS_INCLUDED, 0) * ACCOUNT_MONTHLY;
return (tierBase + extraAccounts) * (1 + (isAccrual() ? ACCRUAL_PCT / 100 : 0)) * (1 - (isQuarterly() ? QUARTERLY_OFF_PCT / 100 : 0));
};
const getSalesTaxFee = () => getStates() * SALES_TAX_STATE_MONTHLY;
const getPayrollFee = () => hasPayroll()
? (payrollPrices.baseMonthly + getPayrollHeads() * payrollPrices.employeeMonthly + getContractors() * payrollPrices.contractorMonthly) * (1 + (isWeekly() ? payrollPrices.weeklyPct / 100 : 0))
: 0;
const getAddonsFee = () => getAddons().reduce((sum, id) => sum + (addonPrices[id] || 0), 0) + (usesApp() ? RECEIPTS_APP_MONTHLY : 0);
const getCfoFee = () => getCfoHours() * CFO_HOURLY;
const getDedicatedFee = () => isDedicated() ? (getBooksFee() + getAddonsFee()) * DEDICATED_PCT / 100 : 0;
const getBeforePromo = () => getBooksFee() + getSalesTaxFee() + getPayrollFee() + getAddonsFee() + getCfoFee() + getDedicatedFee();
const getPromo = () => getBeforePromo() * PROMO_PCT / 100;
const getMonthly = () => Math.round(getBeforePromo() - getPromo());
const getMonthlyPrepaid = () => Math.round(getMonthly() * (1 - ANNUAL_PREPAY_PCT / 100));
 
const getSetupFee = () => softwareStatusFee[getSoftwareStatus()];
const getCatchupFee = () => getMonthsBehind() * getBooksFee() * oneTimePrices.catchupPct / 100;
const getOneTime = () => Math.round(oneTimePrices.onboarding + getSetupFee() + getCatchupFee());
 
const getReturnFee = () => takesTaxReturn() ? getReturnFeeIfTaken() : 0;
const getEstimatesFee = () => takesEstimates() ? returnPrices.estimatesYear : 0;
const getYearEndForms = () => (hasPayroll() ? getPayrollHeads() * payrollPrices.w2Each : 0) + getContractors() * payrollPrices.n1099Each;
const getAnnual = () => Math.round(getReturnFee() + getEstimatesFee() + getYearEndForms());
 
// Descriptions the bookkeeper can quote from
const describeBooks = () => 'Tier ' + getTierNumber() + ', ' + count(getTransactions()) + ' transactions a month, ' + getAccounts() + ' accounts, ' + (isAccrual() ? 'accrual' : 'cash') + ' basis, ' + (isQuarterly() ? 'quarterly' : 'monthly') + ' close';
const describePayroll = () => hasPayroll()
? getPayrollHeads() + ' on payroll (' + getEmployees() + ' employees' + (isOwnerOnPayroll() ? ' plus the owner' : '') + '), ' + (frequencyNames[getFrequency()] || '') + (getContractors() > 0 ? ', ' + getContractors() + ' 1099 contractors' : '')
: getContractors() > 0 ? 'no payroll, ' + getContractors() + ' 1099 contractors' : 'no payroll';
const describeTaxes = () => [
collectsSalesTax() ? 'sales tax in ' + plural(getStates(), 'state', 'states') + (isOverNexus() ? ' (nexus check needed)' : '') : 'no sales tax',
takesTaxReturn() ? getReturn().name + (isK1Entity() ? ', ' + plural(getOwners(), 'K-1', 'K-1s') : '') : 'no annual return',
takesEstimates() ? 'quarterly estimates' : ''
].filter(part => part !== '').join('; ');
const describeService = () => (isDedicated() ? 'dedicated bookkeeper' : 'bookkeeping team') + ', documents: ' + (docsNames[getDocs()] || '')
+ (getAddons().length > 0 ? '; ' + getAddons().map(id => (addonNames[id] || id).toLowerCase()).join(', ') : '')
+ (getCfoHours() > 0 ? '; CFO advisory ' + getCfoHours() + ' h/mo' : '');
const describeStart = () => [
getTiming(),
getSituation() === 'switching' ? 'taking over from the previous bookkeeper' : '',
getMonthsBehind() > 0 ? getMonthsBehind() + ' months of catch-up' : '',
getSetupFee() > 0 ? softwareStatusNames[getSoftwareStatus()].toLowerCase() + ' in ' + getSoftware() : ''
].filter(part => part !== '').join('; ');
 
// Estimate Section
const summarySection = form.addSubform('summary', { title: '💰 Your Estimate', isCollapsible: false });
 
summarySection.addRow(row => {
row.addTextPanel('promoBanner', {
computedValue: () => PROMO_TEXT + ': ' + PROMO_PCT + '% off is already in the monthly fee below.',
customStyles: { 'font-size': '0.95rem', 'font-weight': '500', 'color': '#166534', 'background': '#dcfce7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => getPromo() > 0
});
});
 
summarySection.addRow(row => {
row.addPriceDisplay('booksFee', {
label: () => 'Bookkeeping, tier ' + getTierNumber(),
computedValue: () => Math.round(getBooksFee()),
decimals: 0,
variant: 'default',
suffix: '/mo'
}, '1fr');
row.addPriceDisplay('salesTaxFee', {
label: () => 'Sales tax filings, ' + plural(getStates(), 'state', 'states'),
computedValue: () => getSalesTaxFee(),
decimals: 0,
variant: 'default',
prefix: '+',
suffix: '/mo',
isVisible: () => getSalesTaxFee() > 0
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('payrollFee', {
label: () => 'Payroll, ' + plural(getPayrollHeads() + getContractors(), 'person', 'people'),
computedValue: () => Math.round(getPayrollFee()),
decimals: 0,
variant: 'default',
prefix: '+',
suffix: '/mo',
isVisible: () => getPayrollFee() > 0
}, '1fr');
row.addPriceDisplay('addonsFee', {
label: 'Add-ons',
computedValue: () => getAddonsFee(),
decimals: 0,
variant: 'default',
prefix: '+',
suffix: '/mo',
isVisible: () => getAddonsFee() > 0
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('cfoFee', {
label: () => 'CFO advisory, ' + getCfoHours() + ' h',
computedValue: () => getCfoFee(),
decimals: 0,
variant: 'default',
prefix: '+',
suffix: '/mo',
isVisible: () => getCfoFee() > 0
}, '1fr');
row.addPriceDisplay('dedicatedFee', {
label: 'Dedicated bookkeeper (+' + DEDICATED_PCT + '%)',
computedValue: () => Math.round(getDedicatedFee()),
decimals: 0,
variant: 'default',
prefix: '+',
suffix: '/mo',
isVisible: () => getDedicatedFee() > 0
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('promoDiscount', {
label: PROMO_TEXT + ' (-' + PROMO_PCT + '%)',
computedValue: () => -Math.round(getPromo()),
decimals: 0,
variant: 'success',
suffix: '/mo',
isVisible: () => getPromo() > 0
}, '1fr');
});
 
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
 
summarySection.addRow(row => {
row.addPriceDisplay('monthlyTotal', {
label: 'Every month',
computedValue: () => getMonthly(),
decimals: 0,
variant: 'large',
suffix: 'per month, flat'
}, '1fr');
row.addPriceDisplay('oneTimeTotal', {
label: 'Once at the start',
computedValue: () => getOneTime(),
decimals: 0,
variant: 'highlight',
suffix: 'one-time'
}, '1fr');
row.addPriceDisplay('annualTotal', {
label: 'Once a year',
computedValue: () => getAnnual(),
decimals: 0,
variant: 'highlight',
suffix: 'per year',
isVisible: () => getAnnual() > 0
}, '1fr');
});
 
summarySection.addRow(row => {
row.addTextPanel('prepayNote', {
computedValue: () => money(getMonthlyPrepaid()) + '/mo when twelve months are paid up front (' + ANNUAL_PREPAY_PCT + '% off).',
customStyles: { 'font-size': '0.9rem', 'color': '#059669', 'font-weight': '500' },
isVisible: () => ANNUAL_PREPAY_PCT > 0
});
});
 
// Sticky Summary Section
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
 
finalSection.addRow(row => {
row.addPriceDisplay('monthlyFee', {
label: 'Monthly fee',
computedValue: () => getMonthly(),
decimals: 0,
variant: 'large',
suffix: '/month'
}, '1fr');
row.addPriceDisplay('oneTimeFee', {
label: 'One-time',
computedValue: () => getOneTime(),
decimals: 0,
variant: 'default'
}, '1fr');
row.addPriceDisplay('annualFee', {
label: 'Per year',
computedValue: () => getAnnual(),
decimals: 0,
variant: 'default'
}, '1fr');
});
 
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Estimate only. The engagement letter after the call sets the final fee and scope.',
customStyles: { 'font-size': '0.85rem', 'color': '#94a3b8', 'font-style': 'italic' }
});
});
 
// Contact Section
const contactSection = form.addSubform('contact', { title: '📋 Your Contact Details' });
 
contactSection.addRow(row => {
row.addTextbox('businessName', {
label: 'Business Name',
placeholder: 'Or "not formed yet"',
tooltip: 'Leave it blank if the business does not exist yet'
}, '2fr');
row.addTextbox('ein', {
label: 'EIN (optional)',
placeholder: '12-3456789'
}, '1fr');
});
 
contactSection.addRow(row => {
row.addTextbox('name', {
label: 'Your Name',
placeholder: 'John Smith',
isRequired: true
}, '1fr');
row.addTextbox('city', {
label: 'City and State',
placeholder: 'Columbus, OH',
tooltip: 'We work remotely, but the state decides a lot of the tax picture',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addTextbox('phone', {
label: 'Phone',
placeholder: '(555) 123-4567',
isRequired: true
}, '1fr');
row.addEmail('email', {
label: 'Email Address',
placeholder: 'john@example.com',
tooltip: 'For the estimate and the engagement letter',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addRadioButton('callTime', {
label: 'Best time to call',
options: [
{ id: 'anytime', name: 'Anytime' },
{ id: 'morning', name: 'Mornings' },
{ id: 'afternoon', name: 'Afternoons' }
],
defaultValue: 'anytime'
}, '1fr');
});
 
contactSection.addRow(row => {
row.addTextarea('notes', {
label: 'Anything else we should know?',
placeholder: 'E.g. an IRS notice on the desk, a second location, sales into Canada, plans to hire, a lender asking for statements',
rows: 3,
maxLength: 1500
});
});
 
const whenWeCall = () => {
const when = contactSection.radioButton('callTime')?.value();
return when === 'morning' ? 'in the morning' : when === 'afternoon' ? 'in the afternoon' : 'shortly';
};
 
// PDF estimate: the business described and the three numbers
form.configurePdf('estimate', pdf => {
pdf.configure({
filename: 'bookkeeping-estimate',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the estimate (PDF)',
header: {
title: 'Bookkeeping Estimate',
subtitle: entityNames[getEntity()] + ' | ' + count(getTransactions()) + ' transactions a month | ' + (hasPayroll() ? getPayrollHeads() + ' on payroll' : 'no payroll')
},
footer: {
text: 'Estimate only. The engagement letter after the call sets the final fee and scope.',
showPageNumbers: true
}
});
 
pdf.addSection('Your Business', section => {
section.addRow(row => {
row.addField('Business', contactSection.textbox('businessName')?.value() || 'not formed yet');
row.addField('Entity', entityNames[getEntity()]);
row.addField('Industry', industryNames[getIndustry()]);
row.addField('Annual revenue', 'about ' + money(getRevenue()));
});
section.addRow(row => {
row.addField('Books', describeBooks());
row.addField('Software', getSoftware() + ', ' + softwareStatusNames[getSoftwareStatus()].toLowerCase());
});
section.addRow(row => {
row.addField('Payroll', describePayroll());
row.addField('Taxes', describeTaxes());
});
section.addRow(row => {
row.addField('Service', describeService());
row.addField('Start', describeStart());
});
});
 
pdf.addSection('Every Month', section => {
const rows: string[][] = [['Bookkeeping, tier ' + getTierNumber() + ': ' + describeBooks(), money(getBooksFee())]];
if (getSalesTaxFee() > 0) rows.push(['Sales tax filings, ' + plural(getStates(), 'state', 'states'), money(getSalesTaxFee())]);
if (getPayrollFee() > 0) rows.push(['Payroll: ' + describePayroll(), money(getPayrollFee())]);
if (getAddonsFee() > 0) rows.push(['Add-ons: ' + [usesApp() ? 'receipt capture app' : ''].concat(getAddons().map(id => (addonNames[id] || id).toLowerCase())).filter(part => part !== '').join(', '), money(getAddonsFee())]);
if (getCfoFee() > 0) rows.push(['CFO advisory, ' + getCfoHours() + ' hours', money(getCfoFee())]);
if (getDedicatedFee() > 0) rows.push(['Dedicated bookkeeper (+' + DEDICATED_PCT + '%)', money(getDedicatedFee())]);
if (getPromo() > 0) rows.push([PROMO_TEXT + ' (-' + PROMO_PCT + '%)', '-' + money(getPromo())]);
rows.push(['Monthly fee', money(getMonthly()) + '/mo']);
section.addTable(['Item', 'Amount'], rows);
if (ANNUAL_PREPAY_PCT > 0) section.addText(money(getMonthlyPrepaid()) + '/mo when twelve months are paid up front.');
});
 
pdf.addSection('Once at the Start and Once a Year', section => {
const rows: string[][] = [['Onboarding', money(oneTimePrices.onboarding)]];
if (getSetupFee() > 0) rows.push([softwareStatusNames[getSoftwareStatus()] + ': ' + getSoftware(), money(getSetupFee())]);
if (getMonthsBehind() > 0) rows.push(['Catch-up bookkeeping, ' + getMonthsBehind() + ' months', money(getCatchupFee())]);
if (getReturnFee() > 0) rows.push([getReturn().name + (isK1Entity() && getOwners() > 1 ? ' with ' + getOwners() + ' K-1s' : '') + ' (once a year)', money(getReturnFee())]);
if (getEstimatesFee() > 0) rows.push(['Quarterly estimated taxes (once a year)', money(getEstimatesFee())]);
if (getYearEndForms() > 0) rows.push(['Year-end forms: ' + [hasPayroll() ? getPayrollHeads() + ' W-2s' : '', getContractors() > 0 ? getContractors() + ' 1099-NECs' : ''].filter(part => part !== '').join(', ') + ' (once a year)', money(getYearEndForms())]);
section.addTable(['Item', 'Amount'], rows);
});
 
pdf.addSection('What Happens Next', section => {
section.addText('A bookkeeper calls you ' + whenWeCall() + ', within ' + REPLY_HOURS + ' hours' + (CONSULT_MINUTES > 0 ? '; the first ' + CONSULT_MINUTES + ' minutes are free.' : '.'));
section.addText('We confirm the entity, the return you file, sales tax and payroll, and the scope of the plan. You receive the engagement letter with the final fee and a list of what we need to get started.');
const notes = contactSection.textarea('notes')?.value();
if (notes) {
section.addDivider();
section.addText('Notes: ' + notes);
}
section.addDivider();
section.addText('This estimate is not an offer to contract and not tax advice. What the form says about your return, payroll and sales tax follows from the answers you gave and the thresholds in force today; the bookkeeper confirms it on the call. Software subscriptions are billed by the vendor in your name and are not included.');
});
});
 
form.configureSubmitButton({
label: 'Request My Estimate'
});
 
form.configureCompletionScreen({
type: 'text',
title: 'Your request is with our bookkeeper',
message: 'We will call within ' + REPLY_HOURS + ' hours to confirm the scope. The engagement letter follows the call.'
});
}
 

Frequently Asked Questions

Can I price by transaction tiers the way my firm does?

Yes. The monthly bookkeeping fee comes from four transaction tiers with a per-hundred rate above the top tier, plus a fee for each account beyond the included ones. Change the tier limits and prices at the top of the code to match your plans.

How does the calculator know which tax return the business files?

The entity type decides it: Schedule C for a sole proprietor, Form 1120-S for an S corporation, Form 1065 for a partnership, Form 1120 for a C corporation, and for a nonprofit the 990-N, 990-EZ or full 990 by gross receipts. Each extra owner adds a K-1 fee.

Does it handle payroll correctly for S corp owners?

Yes. An S corporation owner is added to the payroll headcount because the IRS requires a reasonable salary, a sole proprietor or partner is kept off payroll, and a C corporation owner chooses. Weekly pay runs carry a surcharge, and W-2s and 1099-NECs are added once a year.

What about sales tax and online sellers?

Sales tax follows the industry: required for restaurants and e-commerce, a switch for everyone else. An online store with revenue above the economic nexus threshold is warned that it probably owes sales tax in more than one state, and each state adds a monthly filing fee.

Can I show a one-time setup cost and an annual cost separately?

Yes. The estimate shows three numbers: the flat monthly fee, the one-time cost with onboarding, file setup or migration and catch-up months, and the annual cost with the tax return, quarterly estimates and year-end forms. An annual prepay discount is shown beside the monthly fee.