Skip to main content
Glama

Senaro Personal Finance

Credit Card Payoff Calculator

calculate_cc_payoff
Read-onlyIdempotent

Calculation, not advice. Verify with a professional before acting. Calculate credit card payoff timeline with month-by-month amortization. Supports dual APR segments (purchase + cash advance), two strategies (avalanche, snowball), fixed/dynamic payment modes, and an optional schedule of one-time windfalls (tax refund, bonus, inheritance) applied at specific months. Returns debt-free date, total interest, per-card payoff order, first_month_interest_ratio_frac (the first month's interest/payment fraction) and lifetime_interest_ratio_frac (whole-projection total_interest/total_paid fraction), domain warnings emitted as warnings[].type (DEATH_SPIRAL, CA_TRAP, MIN_PAYMENT_TRAP, DAY_ZERO_COST, DEBT_GROWING, PROMO_CLIFF, PREDATORY_RATE, SPEND_EXCEEDS_PAYDOWN, PLAN_TERM_ELAPSED_WITH_BALANCE), separately validation disclosures emitted as warnings[].code (MINIMUM_PAYMENT_NOT_BINDING on the default dynamic-budget path, MINIMUM_PAYMENT_NOT_BINDING_IN_BASELINE when fixed_payments = true; these two shapes coexist in the same warnings[] array but are distinguishable by which keys each entry carries), and when a plan exists (extra_monthly_payment > 0, windfalls present, or fixed_payments = true) a min_payments_only baseline block with interest_saved_vs_min_payments and months_saved_vs_min_payments quantifying the interest and time difference versus paying minimums only. When windfalls are also present the response additionally gains a baseline vs with_windfalls savings block. The response echoes strategy (canonical lowercase 'avalanche' | 'snowball', the strategy the plan was actually simulated under) at the top level. The response includes chart_hints with rendering directives any client can use. The senaro-charts MCP server renders them locally over stdio only. Use output: 'summary' (default) for a compact response or 'inline' for the full payload. output: 'capture' writes the full payload to a file on this server's local disk and returns a capture_ref URI for chart rendering; available on the local stdio transport only, and rejected with a structured error on the hosted HTTP transport, where a capture_ref would be a dead link for a remote caller. Top-level entered_starting_balance echoes the caller's supplied balances by segment type (total, purchase, cash_advance, balance_transfer, promotional, installment_plan), before any fee, recurring spend, or interest posts; total is the sum of the other five. Top-level total_purchase_segment_fees_paid reports the portion of card fees (plan_fees_monthly plus annual_fee accruals) charged to the purchase segment across the projection, also present on every monthly_totals[] and analysis.buckets[] row. These month counts are each a {status, value, explanation} object rather than a bare month count: months_to_payoff, min_payments_only.months, minimums_only_baseline.months, and, when windfalls are supplied, baseline.months and with_windfalls.months. status is "defined" with an integer value when that plan reaches a zero balance, and "none" with value null when it does not, in which case explanation says why. The per-card payoff month inside card_payoff_order[] does not use the {status, value, explanation} object. It is still a bare integer.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
toolArgumentsYesJSON object with these parameters: cards (REQUIRED array, max 20): - name: string (optional, auto-generated if omitted) - purchase_balance: decimal >= 0 (REQUIRED unless segments[] is supplied) - purchase_apr_pct: decimal 0-100 as percentage, e.g. 24.99 not 0.2499 (REQUIRED unless segments[] is supplied) - cash_advance_balance: decimal >= 0 (optional, default 0) - cash_advance_apr_pct: decimal > 0, <= 100 (required only if cash_advance_balance > 0) - minimum_payment: decimal >= 0 (optional, omit or set 0 to auto-calculate). Binds ONLY when fixed_payments = true: the card is then locked at max(minimum_payment, that card's issuer minimum computed at month 1), and that locked amount is paid every month. On the DEFAULT path (fixed_payments = false), this value is NOT used: the plan recomputes each card's issuer minimum from its current balance every month, so the monthly budget is the sum of those recomputed minimums plus extra_monthly_payment. To hold a stated payment instead, set fixed_payments = true. - annual_fee: decimal >= 0 (optional, default 0). Charged at month 1 and every 12th month thereafter while the card has a balance. - plan_fees_monthly: decimal >= 0 (optional, default 0). Charged every month while the card has a balance (e.g. pay-over-time plan fee). - segments: array (OPTIONAL; when present, supersedes purchase_balance / cash_advance_balance). Max 100 segments total across all cards in the request. Per-card, per-type caps: 1 purchase, 1 cash_advance, 5 balance_transfer, 1 promotional, 10 installment_plan. Each item: - type: 'purchase' | 'cash_advance' | 'balance_transfer' | 'promotional' | 'installment_plan' (REQUIRED). Use 'promotional' for a balance whose 0% (or below-market) intro APR reverts to a standard rate later and is NOT a balance transfer, e.g. a 0% intro purchase APR; it validates and reverts identically to balance_transfer. - balance: decimal >= 0 (REQUIRED) - apr_pct: decimal 0-100 (REQUIRED for purchase / cash_advance / balance_transfer / promotional; must be 0 for installment_plan; for cash_advance, must be > 0 when balance > 0, since cash advances have no promotional 0% product) - revert_apr_pct: decimal 0-100 (REQUIRED for balance_transfer / promotional; APR after promo expires; not applicable to purchase / cash_advance (silently ignored); REJECTED on installment_plan (FIELD_NOT_ALLOWED_HERE) since installment_plan has no post-term-rate concept: an expired plan's residual keeps its existing rate) - promo_expires_month: int >= 1 (REQUIRED for balance_transfer / promotional; month at which apr_pct flips to revert_apr_pct) - monthly_fee: decimal >= 0 (REQUIRED for installment_plan; fixed fee charged each month while the plan's term is active; stops accruing once the term elapses) - plan_payment_due: decimal > 0 (REQUIRED for installment_plan; locked monthly payment, principal + monthly_fee; keeps being charged every month even after the term elapses, until the balance clears) - remaining_payments: int >= 0 (optional, installment_plan only). 0 means the plan's stated term has ALREADY ended: this is the correct, valid way to model an already-expired plan carrying a residual balance, and it is accepted regardless of balance (an expired plan's residual is a real state, not a data-entry error). When remaining_payments >= 1, it must be consistent with balance / plan_payment_due / monthly_fee: net = plan_payment_due - monthly_fee must be positive, or the plan could never amortize (rejected as INSTALLMENT_PLAN_NET_NOT_POSITIVE); balance must be <= remaining_payments * net (the plan must be able to clear within its stated term) and balance must be > (remaining_payments - 1) * net (the term must not be overstated), either violation rejected as INSTALLMENT_PLAN_TERM_MISMATCH. If a plan has already ended, enter remaining_payments as 0 rather than a positive count that cannot clear the balance. An expired plan (remaining_payments reaches 0 with a positive balance still owed, whether supplied that way or reached by simulation) does not freeze and is never repriced: it keeps receiving its locked plan_payment_due every month at its existing rate until the balance clears. One aggregated PLAN_TERM_ELAPSED_WITH_BALANCE warning fires per card (never one per segment), naming every affected plan on that card and its residual, plus the card total. - merchant_name: string (optional, installment_plan only) - min_payment_order: int >= 1 (optional; the minimum first pays any installment_plan segment's locked plan_payment_due in full, structurally, regardless of APR or this value; among the remaining (revolving) segments it then pays down the lowest-current-APR segment first, issuer practice, not statutorily regulated; this value only breaks a tie between segments that share both the exact same current APR AND the same installment-plan status, in which case the higher-balance segment wins; two segments on the same card sharing a value emit a DUPLICATE_PAYMENT_ORDER warning; a supplied value with no APR-tie partner to ever decide emits PAYMENT_ORDER_NOT_DECISIVE) - extra_payment_order: int >= 0 (optional; 0 = SKIP -- this segment never receives extra payment, the default for installment_plan; a positive value only breaks a tie between segments that share the exact same current APR, since the Credit CARD Act (TILA §164(b), 15 U.S.C. §1666c(b)) / 12 CFR §1026.53(a) always pays the highest-current-APR segment first regardless of this value; two segments on the same card sharing a positive value emit a DUPLICATE_PAYMENT_ORDER warning, duplicate 0s never warn; a supplied value with no APR-tie partner to ever decide emits PAYMENT_ORDER_NOT_DECISIVE -- this hits installment_plan hardest, since it is pinned at 0% APR (apr_pct is not read for this segment type), so a positive override on it can win the tie-break only against another segment also at exactly 0%; whenever any positive-APR segment shares the card, the plan stays ELIGIBLE and sorts LAST behind every positive-APR segment, receiving extra only once those higher-APR balances clear) Response gains per-card interest_saving_balance (sum of APR-bearing segments) and installment_balance_locked (sum of installment_plan segments) when segments[] is supplied. extra_monthly_payment: decimal >= 0 (optional, default 0) strategy: 'avalanche' | 'snowball' (optional, default 'avalanche') fixed_payments: bool (optional, default false). When true, each card's payment is locked at max(cards[].minimum_payment, that card's issuer minimum computed at month 1), and the total monthly budget (sum of those locked amounts plus extra_monthly_payment) is held constant for the whole payoff, so once a card is paid off its freed-up payment rolls forward onto the remaining cards. This is the ONLY mode in which cards[].minimum_payment affects the result; see the minimum_payment field above for what happens on the default path. apply_rate_cap: bool (optional, default false) full_schedule: bool (optional, default false). The per-card monthly_schedule is capped at 1000 rows total (cards x months); on a long multi-card payoff later months are omitted from monthly_schedule. The portfolio-level monthly_totals series is always complete, so read monthly_totals for the full timeline. per_segment: bool (optional, default true) include_card_timeline: bool (optional, default false) windfalls: array of one-time principal payments (optional, default empty, max 12 items). Each item: - month: int >= 0 (REQUIRED). 0 means applied before month 1's interest (same as reducing starting principal). N >= 1 applies at the END of calendar month N, so month N+1's interest is calculated on the post-windfall balance. - amount: decimal > 0 (REQUIRED). - label: string (optional). e.g. 'Tax refund', 'Year-end bonus'. When any windfalls are present the response gains: baseline, with_windfalls, months_saved_vs_baseline, interest_saved_vs_baseline, windfalls_applied[], windfalls_unused[]. Empty windfalls yields the same response shape as before. Min-payments-only baseline (v1.7, gap #593): when extra_monthly_payment > 0, windfalls are present, or fixed_payments = true, the response also gains: - min_payments_only: { months, interest_cost, amount_paid, payoff_date } - what happens paying minimums only - interest_saved_vs_min_payments: decimal - total interest saved vs paying minimums only (always >= 0) - months_saved_vs_min_payments: int - months saved vs paying minimums only (when minimums never pay off, capped at MaxMonths minus with-plan months) Trivial minimums-only calls (no extra, no windfalls, not fixed) keep their current response shape unchanged. Minimums-only baseline series (v1.9, B2; bucketed v2.5, #1108): under the same plan-exists condition as min_payments_only above, the response also gains minimums_only_baseline: { months, granularity, buckets[], payoff_date } - the minimums-only balance trajectory bucketed through the same shared aggregator as analysis.buckets[] (same bucket row shape: bucket_index, label, plus the 17 money fields), with granularity auto-selected from THIS series' own term, the same standard rule analysis.granularity itself uses. minimums_only_baseline.granularity is NOT guaranteed to equal analysis.granularity: each series auto-selects independently from its own term, so read each series' own granularity rather than assuming they match. Equal bucket-array length is NOT a valid signal that the two granularities match either: a 24-month primary run (monthly, 24 buckets) and a 24-year minimums-only baseline (yearly, 24 buckets) land on the SAME buckets[].length with DIFFERENT granularities. granularity is the only valid mismatch signal; length is not. Computed from the SAME gated simulation as min_payments_only, so months/payoff_date agree between the two blocks. Absent under the identical trivial-call condition as min_payments_only. chart_title: string (optional) override for the chart title. Must not contain em-dashes or en-dashes. Max 120 characters. output: 'summary' | 'inline' | 'capture' (optional, default 'summary') 'capture' is available on the local stdio transport only; the hosted HTTP transport rejects it with a structured error naming 'summary' and 'inline' as the valid alternatives.

Schema Changelog

Changes observed during successful MCP inspections.

  1. First observed

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnly/idempotent/non-destructive, so the bar is lower. The description adds real behavioral context: the transport-specific rejection of 'capture' on HTTP, the significance of {status, value, explanation} objects, the 1000-row full_schedule cap, and the coexistence of two warning shapes. It is verbose but informative rather than contradictory.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a wall of dense prose packing warning codes, ratio field names, and margin enumeration into continuous sentences. Front-loading is reasonable, but the sheer length and low signal-to-token ratio make it hard to scan; much of the response-shape detail belongs in an output-schema or structured doc rather than prose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema and a single nested argument envelope, the description goes a long way to describing the return shape: payoff date, interest totals, warning arrays, baseline blocks, chart_hints, and output-mode behavior. The inclusion of all nine warning types plus validation codes is thorough, though scattered.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents the single toolArguments envelope and all nested fields in depth. The description adds value on warning-shape coexistence and baseline blocks, but does not introduce parameter semantics beyond what the schema provides, so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource ('Calculate credit card payoff timeline with month-by-month amortization') and explicitly enumerates the distinguishing capabilities (dual APR segments, avalanche/snowball, fixed/dynamic payments, windfalls). An agent can immediately separate this from siblings like calculate_loan_payoff or compare_strategies.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

States the tool's scope ('Calculation, not advice') and names the companion server senaro-charts for rendering, plus the transport-specific rules for 'capture'. However it does not explicitly state when to prefer this over calculate_loan_payoff or compare_strategies, so sibling routing is left implicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources