1. Overview #
MoneyThumb (PDF Insights TP, API v1.5) ingests bank statement PDFs and a Plaid asset report and returns a structured scorecard of revenue metrics, account summaries, debt indicators, and per-statement detail. It is a downstream underwriting analysis subsystem β never runs during intake.
Once a deal has classified bank statements, the pipeline creates an MT app, uploads PDFs, polls for results, then fans out to:
- Deal properties β orchestration status, revenue statistics (production-final), 3m/6m/all aggregates, and
_extractedraw values. - Bank Statement custom object (
2-37801779) β one record per (statement-month Γ account). - MCA Position custom object (
2-38036513, internal namelender_metrics) β one consolidated record per lender. - UW Documents custom object (
2-56466936) β four MT file outputs (CSV, Excel scorecard, log, summary PDF). - Associated Company β gap-fill for DBA, address, ZIP+4, primary bank.
Two parallel write paths exist (see Β§2). Property catalog reference: reference/hubspot/hubspot_properties_deals.json.
2. Sync mechanism summary #
MT API β moneyThumbWebhook OR pollMoneyThumbResults
β results stored at deals/{dealId}/moneythumb_results/latest
β syncMoneyThumbToHubSpot() (full-refresh orchestrator)
ββ clearHubspotDeal() [archive prior bank statements + MCA positions, blank revenue props]
ββ moneythumbService.makeExcel() [fetch xlsx scorecard]
ββ upload JSON + XLS to GCS [public URLs]
ββ syncBankStatementsToHubSpot() [batch upsert by tid]
ββ syncMcaPositionsToHubSpot() [batch upsert by lender_name]
ββ extractRevenueStatistics() + computeMoneythumbAggregates()
β β PATCH deals/{id}: revenue stats + 3m/6m/all aggregates + average_monthly_revenue__final_
ββ computeCompanyAddressFromAppinfo() [PATCH company: only fill empty fields]
ββ enrichmentService.enrichFields() [Firestore-side enrichment, conf 0.95]
Parallel path (poller only):
β extractAndStoreDealProperties() [Firestore deals/{id}.extractedFields.*]
β syncExtractedFieldsToHubSpot() [PATCH *_extracted props]
β enrichCompanyFromMoneyThumb() [DBA + address gap-fill on company]
β processMoneyThumbFiles() [4 UW Documents]
| Trigger | Entry point | Calls full orchestrator? | Calls extracted-fields sync? |
|---|---|---|---|
| MT webhook | functions/moneythumb/moneyThumbWebhook.js | yes (syncMoneyThumbToHubSpot) | no |
| 2-min poller | functions/moneythumb/pollMoneyThumbResults.js | no β uses syncExtractedFieldsToHubSpot only | yes |
Manual HTTP syncResultsToHubSpot | functions/moneythumb/syncResultsToHubSpot.js | yes | yes |
Practical implication: the canonical MT sync is the orchestrator. The poller writes a strictly smaller set of fields (the _extracted family).
3. RAW EXTRACTED _extracted suffix #
Set by syncExtractedFieldsToHubSpot() (poller + manual sync only). Source = Firestore deals/{id}.extractedFields.*. UW-editable; locked fields are skipped on next sync.
| HubSpot property | Firestore field | Type | Transform | Set in (file:line) |
|---|---|---|---|---|
moneythumb_app_url_extracted | moneythumbAppUrl | string | identity | extractedFieldsSyncHandler.js:64 |
moneythumb_spreadsheet_url_extracted | moneythumbSpreadsheetUrl | string | identity | extractedFieldsSyncHandler.js:65 |
avg_monthly_balance_extracted | averageMonthlyBalance | number | round 2dp | extractedFieldsSyncHandler.js:55, sourced syncMoneyThumbResultsToHubSpot.js:283 |
true_avg_monthly_revenue_extracted | trueAverageMonthlyRevenue | number | round 2dp; from averageMonthlyRevenue6Months | extractedFieldsSyncHandler.js:56 |
true_avg_monthly_revenue_last_3_months_extracted | trueAverageMonthlyRevenueLast3Months | number | round 2dp | extractedFieldsSyncHandler.js:57 |
true_avg_monthly_revenue_last_4_months_extracted | trueAverageMonthlyRevenueLast4Months | number | round 2dp | extractedFieldsSyncHandler.js:58 |
true_avg_monthly_revenue_last_6_months_extracted | trueAverageMonthlyRevenueLast6Months | number | round 2dp | extractedFieldsSyncHandler.js:59 |
true_avg_monthly_revenue_last_12_months_extracted | trueAverageMonthlyRevenueLast12Months | number | round 2dp | extractedFieldsSyncHandler.js:60 |
nsf_days_extracted | nsfDays | number | identity; from nsfCount | extractedFieldsSyncHandler.js:61 |
low_days_extracted | lowDays | number | identity | extractedFieldsSyncHandler.js:62 |
credit_counts_extracted | creditCounts | number | identity | extractedFieldsSyncHandler.js:63 |
4. DERIVED / SYNCED production-final #
Written by syncMoneyThumbToHubSpot() orchestrator in a single PATCH at syncMoneyThumbResultsToHubSpot.js:234.
4a. Status / orchestration
| HubSpot property | Type | Source / value | Set / Cleared |
|---|---|---|---|
moneythumb_app_id | number | MT API appid | syncMoneyThumbResultsToHubSpot.js:217 |
moneythumb_app_status | enum | 'Results Synced' on success; 'Statements Cleared/Reset' on clear; 'Ready for Moneythumb' on intake gating | :218 / clearHubspotDeal.js:67 / matchCompanyContact.js:298 |
moneythumb_status_messages | html | completion message + appId | :219 / clearHubspotDeal.js:68 |
moneythumb_data_as_of_date | date | today UTC midnight (ms epoch) | :232 / cleared :70 |
moneythumb_spreadsheet_url | string | public GCS URL for XLS | :226 / cleared :69 |
bank_statements_status | enum | 'Statements Complete' on sync | :220 |
moneythumb_app_status enum domain: Not Ready for Moneythumb, Ready for Moneythumb, App Created, Upload Started, Upload Complete, Results Synced, Results Refreshed, Statements Cleared/Reset, Error. Bold values are written by the orchestrator/clear; intermediates are written upstream during upload.
4b. Revenue Statistics (deal-level)
Set by extractRevenueStatistics(). Source = scorecard.revenue_statistics[] rows keyed by label.
| HubSpot property | MT label | Column | Transform |
|---|---|---|---|
revenue | Revenue | monthly | parseFloat |
true_revenue | True Revenue | monthly | parseFloat |
expenses | Expenses | monthly | parseFloat |
profit | Profit | monthly | parseFloat |
balance_days_negative | Balance/Days Negative | monthly | parseFloat |
non_true_revenue | Non-True Revenue | monthly | parseFloat |
true_balance | True Balance | monthly | parseFloat |
average_net_monthly_revenue__moneythumb_ | Average Monthly Net Revenue | monthly | parseFloat |
minimum_monthly_true_revenue | Minimum Monthly True Revenue | monthly | parseFloat |
average_monthly_credit_card_revenue | Average Monthly Credit Card Revenue | monthly | parseFloat |
average_monthly_factoring_revenue | Average Monthly Factoring Revenue | monthly | parseFloat |
combined_days_negative | Combined Days Negative | monthly | parseFloat |
low_days | Low Days | annual | parseInt |
days_with_returns (label "NSF Days") | Days with Returns | annual | parseInt |
mca_withhold_percent_ | MCA Withhold Percent | annual | parseFloat Γ· 100 |
moneythumb_avg_true_credits_6m | nested: scorecard.average_true_revenue_6_month.data Total row β groups[0].trueRevenueAverage | parseFloat | |
4c. Aggregates (3m / 6m / all)
Set by computeMoneythumbAggregates(). Source = per-month combined_account_summary array (oldest β newest).
| HubSpot property | Source row field | Aggregation | Window |
|---|---|---|---|
moneythumb_avg_true_credits_3m | true_credits | average | last 3 months |
moneythumb_avg_monthly_deposits_3m | total_credits | average | last 3 months |
moneythumb_avg_daily_balance_3m | avg_balance | average | last 3 months |
moneythumb_avg_true_debits_3m | true_debits | average | last 3 months |
moneythumb_negative_days_3m | days_neg | sum | last 3 months |
moneythumb_total_nsfs_3m | num_nsfs | sum | last 3 months |
moneythumb_nsf_count_3m | num_nsfs | sum | last 3 months (duplicate of total_nsfs_3m) |
moneythumb_avg_monthly_deposits_6m | total_credits | average | last 6 months |
moneythumb_avg_true_credits_all | true_credits | average | all months |
moneythumb_avg_monthly_deposits_all | total_credits | average | all months |
moneythumb_average_daily_balance | avg_balance | average | all months |
moneythumb_total_nsfs_all | num_nsfs | sum | all months |
moneythumb_beginning_balance | starting_balance | first row | first month |
moneythumb_ending_balance | ending_balance | last row | last month |
moneythumb_lowest_daily_balance | min_balance | min over rows | all months |
moneythumb_lowest_monthly_balance | avg_balance | min over rows | all months |
moneythumb_month_with_lowest_balance | month string | row with min avg_balance | all months |
average_monthly_revenue__final_ | revenue_statistics[label='True Revenue'].monthly, fallback label='Revenue' | identity | β |
All numeric values rounded to 2 decimal places via round2(). null results dropped from PATCH.
5. MT Reports as UW Documents 2-56466936 #
Created by processMoneyThumbFiles() via syncDocumentToHubSpot(). All four collapse to document_type = 'MoneyThumb Report'.
Firestore docType | HubSpot document_type | Source | File name |
|---|---|---|---|
moneythumb_csv | MoneyThumb Report | results.csvurl | MoneyThumb_Analysis_{dealId}.csv |
moneythumb_report | MoneyThumb Report | results.logurl | MoneyThumb_Log_{dealId}.txt |
moneythumb_excel | MoneyThumb Report | moneythumbService.makeExcel(appId) | MoneyThumb_Scorecard_{dealId}.xlsx |
moneythumb_summary | MoneyThumb Report | locally generated (pdfkit) | MoneyThumb_Summary_Report_{dealId}.pdf |
Rollup: number_of_uw_docs__moneythumb counts these. Final HubSpot UW Document name format: {dealId}-MoneyThumb Report.{ext} with (N) suffix on duplicates.
6. Bank Statement object 2-37801779 #
Created by syncBankStatementsToHubSpot(). One record per (account, statement_month). Existing records matched by moneythumb_tid__base_ and upserted via batch.
| Property | Source | Notes |
|---|---|---|
institution_name | {accountOwner} - {accountNumber} - {month} | composite display key; primary property |
account | accountNumber | |
statement_month | scorecard month | |
total_credits / total_debits | combined_account_summary.total_credits / .total_debits | parseFloat |
average_balance | avg_balance | parseFloat |
starting_balance / ending_balance | same names from MT | parseFloat |
nsf_counts / credit_counts / debit_counts | num_nsfs / num_credits / num_debits | parseInt; default 0 |
true_credits | true_credits | parseFloat |
account_owner | first accountOwner[] element | title-cased |
bank_name | bankName from statement_summaries | |
start_date / end_date | ISO from statement_summaries | UTC-midnight ms-epoch |
as_of_date | same as end_date | |
moneythumb_tid__base_ | tid | idempotency key |
app_id | MT appId | |
associated_company_id / associated_deal_id | resolved via dealβcompany assoc |
Rollup feeders: source for the deal-level n3_month_avg_true_revenue and n4_month_avg_true_revenue rollups (formulas defined in HubSpot UI).
7. MCA Position object 2-38036513 (lender_metrics) #
Created by syncMcaPositionsToHubSpot(). One consolidated record per lender per deal. Existing records matched by moneythumb_app_id + associated_deal_id.
Source aggregation merges three MT inputs: mca_companies[] (totals), monthly_mca[] (per-month), and sections.mca.data (flattened detail). Per-lender aggregates sum monthly amounts/counts, falling back to mca_companies totals when monthly data is missing.
| Property | Source | Transform |
|---|---|---|
lender_name | {lenderName} - {dealId} | composite to allow same lender across deals |
month | hardcoded 'Totals' | sentinel for the rollup record |
totals_position_ / exclude_position_ | true / false | flag |
deposit_total / net_amount_funded | aggregate | Math.abs() |
withdrawal_total | aggregate | Math.abs() |
avg_withdrawal_amount / debit_amount | withdrawal_total / withdrawal_count if count>0, else lastwithdrawalamount | Math.abs() |
withhold_percent | mca_companies.withholdpercent e.g. "15%" | strip %, Γ· 100 |
term_in_days | mca_companies.term | parseFloat |
debit_frequency | mca_companies.withdrawalfrequency | mapped: Daily / Weekly / Bi-Weekly / Monthly / Other / Unknown |
daily_debit_equivalent | debit_amount Γ· {1, 7, 14, 30} | by frequency |
monthly_debit_equivalent | daily_debit_equivalent Γ 30 | |
deposit_count / withdrawal_count | aggregate | parseInt |
last_deposit_date / date_funded | lastdepositdate | UTC-midnight ms-epoch |
last_withdrawal_date | lastwithdrawaldate | UTC-midnight ms-epoch |
moneythumb_app_id / associated_deal_id / associated_company_id | identifiers |
Rollup feeder: source for number_of_associated_mca_positions.
8. HubSpot rollups not written by code #
Computed by HubSpot itself. The integration relies on them but never PATCHes them.
| Property | Type | Rolls up from | Used by |
|---|---|---|---|
number_of_associated_mca_positions | calculation_rollup | MCA Position custom object | cobalt_funding_position derivation (intakeRequiredDocumentStatus.js:39) |
number_of_uw_docs__moneythumb | calculation_rollup (presumed) | UW Documents where document_type='MoneyThumb Report' | intake_required_document_status threshold (intakeRequiredDocumentStatus.js:80,84) |
n3_month_avg_true_revenue | calculation_rollup | Bank Statement object | revenue/offer calc group (HubSpot UI) |
n4_month_avg_true_revenue | calculation_rollup | Bank Statement object | revenue/offer calc group (HubSpot UI) |
moneythumb_app_url | calculation_equation | moneythumb_app_id | UW UI deep-link to MT app |
number_of_uw_docs_statements_ | calculation_rollup | UW Documents where document_type='Bank Statement' | dealinformation |
Implication: rollup propagation is asynchronous β after the orchestrator completes, rollups may briefly show stale values.
9. Status / state properties orchestration #
| Property | Type | Domain | Writers |
|---|---|---|---|
moneythumb_app_status | select | (see above) | orchestrator (βResults Synced), clear, matchCompanyContact.js (βReady with regression guard) |
moneythumb_status_messages | html | free text | orchestrator + clearHubspotDeal |
moneythumb_status_changelog | html | free text | no in-repo writer found β workflow-driven |
moneythumb_data_as_of_date | date | UTC midnight | orchestrator on success; cleared on reset |
moneythumb_update_timestamp | datetime | last sync | no in-repo writer found |
moneythumb_source | select | Plaid Asset Report (Auto), Bank Statements (Auto), Bank Statements (Manual) | cleared on reset; no in-repo writer found |
moneythumb_refresh_results_ | bool | UW-toggled | UW manual flag, consumed by HubSpot workflow |
upload_plaid_to_moneythumb_ | bool | UW-toggled | UW manual flag, consumed by HubSpot workflow |
upload_statements_to_moneythumb_ | bool | UW-toggled | UW manual flag, consumed by HubSpot workflow |
deal_checklist___moneythumb_mca_stats_complete | bool | progress flag | no in-repo writer found |
deal_checklist___moneythumb_revenue_stats_complete | bool | progress flag | no in-repo writer found |
10. Calculations end-to-end #
10a. average_monthly_revenue__final_ β the canonical "verified" revenue
MT scorecard.revenue_statistics[]
ββ row { label: 'True Revenue', monthly: 47235.62 } β preferred
ββ row { label: 'Revenue', monthly: 51000.00 } β fallback
β
βΌ
computeMoneythumbAggregates() [moneythumb/helpers/computeMoneythumbAggregates.js:117-124]
β
βΌ
PATCH deals/{id} { average_monthly_revenue__final_: 47235.62 }
β
βΌ
offer-calculator GET /getDealData [functions/offerCalculator/index.js:582,638]
β
βΌ
formData.monthlyRevenue = parseFloat(deal.average_monthly_revenue__final_) || 0
β
βΌ
calculator.js β suggested_offer_amount, recommended_term, etc.
This is the field that drives the offer calculator's monthly revenue input for both Cobalt and Magenta. The lone numeric MT field consumed by the calc.
Note: average_monthly_revenue__sync_ (group self-reported) is the self-reported revenue, never written by MT code. The offer calculator uses __final_.
10b. cobalt_funding_position
MCA Position custom-object records (created by syncMcaPositionsToHubSpot)
β (HubSpot async rollup)
βΌ
deal.number_of_associated_mca_positions (calculation_rollup)
β
βΌ
deriveFundingPosition(mcaCount) [intakeRequiredDocumentStatus.js:35]
β
βΌ
position = min(mcaCount + 1, 6) β '1'..'6'
β
βΌ
recomputeAndSyncIntakeRequiredDocumentStatus() [intakeRequiredDocumentStatus.js:165-181]
β
βΌ
PATCH deals/{id} { cobalt_funding_position: <derived> } β ONLY when current value is empty
β
βΌ
offer-calculator: fundingPosition = parseInt(deal.cobalt_funding_position) || 1
β
βΌ
HubDB cfs_funding_position lookup β term_deduction, max_term, sp_deduction
Critical rule: UW may set cobalt_funding_position manually. The derivation must never overwrite a non-empty value. Regression test at intakeRequiredDocumentStatus.test.js:334.
10c. NSF / overdraft / negative-day chain
The Firestore extracted-fields path also computes a synthetic bankAnalysisRiskScore + bankAnalysisRiskFactors in extractFinancialMetrics(), but these go to Firestore only β NOT pushed to any HubSpot deal property by current sync handlers.
11. Consumers / dependents #
11a. Offer Calculator
average_monthly_revenue__final_β sole MT-derived input for Cobalt CFS + Magenta MF (configs/cobalt.config.js:73,configs/magenta.config.js:70,index.js:582,638).cobalt_funding_positionβ input viacobalt.config.js:76,index.js:585,644. Derived from MCA-Position rollup when empty.
11b. intake_required_document_status aggregate
intakeRequiredDocumentStatus.js:80-87 requires for complete:
- All five association sub-statuses are
complete number_of_associated_uw_docs__applications >= 1number_of_associated_uw_docs__bank_statements >= 3number_of_uw_docs__moneythumb >= 1moneythumb_app_status === 'Results Synced'
If MT is configured but never completes, the deal cannot reach complete intake status.
11c. Intake-time gating (matchCompanyContact)
matchCompanyContact.js:278-318 and :430-466: when the deal becomes ready for MT, set moneythumb_app_status='Ready for Moneythumb' β but only if the current value is not in the in-progress/done set. Prevents intake re-runs from regressing a completed MT workflow.
11d. Document classification
geminiClassifier.js and genkit/classifyDocument.js recognize moneythumb_report. Files matching moneythumb, scorecard, or summary_report classified directly without LLM (classifyDocuments.js:69-70).
11e. Deal-stage advancement
No code in this repo directly advances a deal stage based on MT fields. Stage advancement is HubSpot-workflow driven.
12. Firestore companion data (not HubSpot) #
| Path | Contents |
|---|---|
moneythumb_apps/{dealId} | App metadata: appId, appNumber, documentIds, webhookUrl, status |
moneythumb_apps/{dealId}/versions/{ISO} | Per-sync snapshot: scorecard URL, JSON URL |
moneythumb_jobs/{appId} | Job lifecycle: status, statementsUploaded, completedAt, completionMethod |
deals/{dealId}/moneythumb_results/latest | Full MT results JSON |
deals/{dealId}/moneythumb_results/company_enrichment | Company-enrichment audit trail |
deals/{dealId}.extractedFields.* | Per-field value with lastSyncedToHubSpot, locked, confidence |
deals/{dealId}.moneythumbBankStatementIds | HubSpot Bank Statement IDs (cleanup on next refresh) |
deals/{dealId}.moneythumbMcaPositionIds | HubSpot MCA Position IDs (cleanup) |
deals/{dealId}.moneythumbResultsSyncedAt | ISO timestamp of last orchestrator run |
enrichmentService.enrichFields(dealId, enrichFields, 'moneythumb', 0.95) writes a parallel store with confidence scoring β Firestore-side only.
13. Gotchas / nuances #
- Two write paths, different field sets. Webhookβorchestrator writes the production-final values + 3m/6m/all aggregates +
average_monthly_revenue__final_. Poller writes only the_extractedfamily. If only_extractedvalues are populated, the webhook never fired (orsyncResultsToHubSpotwas not invoked). moneythumb_nsf_count_3mandmoneythumb_total_nsfs_3mare identical. Bothsum(last_3.num_nsfs). Pick one consumer-side; the duplication is intentional for backwards-compat.clearHubspotDealdoes NOT blank the 3m/6m/all aggregates or balance fields. Only blanks revenue-statistics group +moneythumb_avg_true_credits_6m. If the new sync produces fewer rows, older 3m/6m/all values may persist as stale.- HubSpot rollup propagation lag.
number_of_associated_mca_positions,number_of_uw_docs__moneythumb, andn3/n4_month_avg_true_revenueupdate asynchronously. A sync that just completed may show stale rollups for tens of seconds. cobalt_funding_positionis gap-fill only. UW manual values must never be overwritten by the derivation. Verified by regression test.moneythumb_app_statusregression guard. Intake-side handlers will not reset toReadyif the status is inApp Created,Upload Started,Upload Complete,Results Synced,Results Refreshed, orError.average_monthly_revenue__final_group isverified_info, notmoneythumb. Intentional β the canonical UW input regardless of upstream source.- Historic enum values may differ.
moneythumb_status_changelogmay include older labels likeResults Synced to HubSpot. Catalog updated; changelog stays historical. number_of_uw_docs__moneythumbis missing from the cached property catalog but exists in the live portal. Runnpm run hubspot:sync:propertiesto refresh.- MT prefixes owner names with "DBA " β the company-enrichment path strips it and title-cases (
enrichCompanyFromMoneyThumb.js:56). - PDFs under 50 KB are silently rejected by MT. Pre-validate at upload β see playbook LESSONS #3.
_extractedproperties are UW-editable. Each PATCH first readsmetadata.lockedand skips locked fields.
14. Open questions #
- Who writes
moneythumb_status_changelog? No in-repo PATCH found. Likely a HubSpot workflow appending eachmoneythumb_app_statuschange. Confirm with portal admin. - Who writes
moneythumb_update_timestamp? No in-repo writer. - Who writes
moneythumb_source? Cleared byclearHubspotDeal.js:71but never set in code. Presumably a workflow inferring source from upload toggles. - Who writes
moneythumb_report_pdf? String labeled "Moneythumb Report PDF" in catalog. Whether this stores a URL to the summary UW Document is unclear from code. - Who writes
deal_checklist___moneythumb_mca_stats_complete/_revenue_stats_complete? Likely workflow-driven. - Should
clearHubspotDealalso blank the 3m/6m/all aggregates and balance fields? Currently does not β gap if next sync produces fewer months. - Are
_extractedand_syncedversions ever expected to differ? In principle they should track unless UW edits the_extractedand locks it. No automated reconciliation report.
15. Reference quick-look #
- Bank Statement:
2-37801779 - MCA Position:
2-38036513(internal namelender_metrics) - UW Documents:
2-56466936
- CFS Deal:
813089417β CFS UW Docs:860640945(stage1284463963) - MF Deal:
142412149β MF UW Docs:860639775(stage1284472687)
- Base:
https://insights.moneythumb.com/api/v1.5 - Header:
MT-Product: pdfinsightstp - Secret: GCP Secret Manager
MONEYTHUMB_API_TOKEN
- Source markdown:
docs/integrations/moneythumb-field-map.md - Playbook:
playbooks/moneythumb-playbook/INTEGRATION.md - Production status:
docs/MoneyThumb-Production-Status.md - Blueprint Β§7.2:
docs/blueprint/COBALT_BLUEPRINT.md