estimate_fractional_cto_cost: What the Tool Returns

Anthony Wentzel
Founder, Pineapples

estimate_fractional_cto_cost: What the Tool Returns
estimate_fractional_cto_cost returns the published BLS loaded range for a Computer and Information Systems Manager, and the time-fraction cost of the days or hours you pass. The optional monthly figure is yours. The tool does not quote a Pineapples price, and it does not invent a number when the band or the time is missing.
What does estimate_fractional_cto_cost return to an AI agent?
The tool is read-only. readOnlyHint is true. execute records the call, then runs the wrapper. A throw becomes one sentence of text: "The estimate could not be calculated."
execute: async (input) => {
pushWebmcpToolCall("estimate_fractional_cto_cost")
try {
return estimateFractionalCtoCost(input)
} catch {
return textContent("The estimate could not be calculated.")
}
},
estimateFractionalCtoCost in lib/webmcp/estimate-fractional-cto.ts builds the text the agent reads. The first lines are always the band, the full-time loaded range, the time-fraction cost, and the time fraction.
const lines = [
`Revenue band: ${result.revenueBandLabel}.`,
`Full-time loaded range: ${formatDollarRange(result.loadedAnnualLow, result.loadedAnnualHigh)} a year (${wageSpan}).`,
`Time-fraction cost: ${formatDollarRange(result.fractionalAnnualLow, result.fractionalAnnualHigh)} a year (${formatDollarRange(result.fractionalMonthlyLow, result.fractionalMonthlyHigh)} a month).`,
timeLine,
]
Three lines are conditional. If the time fraction is above 1, the text says the time-fraction estimate is above the fully loaded full-time cost. If a quote was sent and is not a finite number of zero or more, the text says the quote was not used. If the quote is usable, the text adds the annual difference: full-time loaded cost minus the quote times 12, and "Positive means full-time is higher." Omit the quote and that line stays off.
Then every response appends the OEWS source line, the ECEC source line, the assumptions, the calculator URL, and "Estimate only. Not a quote."
Scope and engagement stay out of that text. The wrapper copies DEFAULT_INPUTS, so the labels stay "Architecture and vendor oversight" and "Monthly retainer". The schema has no fields for them. calculateFractionalCtoCost keeps the dollars on the wage, the load, and the time fraction.
pushWebmcpToolCall in lib/webmcp/analytics.ts pushes webmcp_tool_call with tool_name and page_path only when dataLayer already exists. If window is missing, or dataLayer is missing, it returns. I have no call count. I will not publish one.
How the tool is registered, and what the other two tools do, is the overview: WebMCP: How We Made pineapples.dev Usable by AI Agents. This page is this tool only.
Why does it use BLS OEWS May 2025 SOC 11-3021 and the ECEC June 2026 benefits load?
The wage is the national OEWS figure for Computer and Information Systems Managers, SOC 11-3021, May 2025. That is the published occupation for this seat. The comment at the top of lib/fractional-cto-cost.ts says every wage and benefits figure was read from bls.gov, and not to replace them with a market rate or a Pineapples rate.
The annual wages in OEWS are a mean of $192,160, a 75th percentile of $220,730, and a 90th percentile of $297,510. The tables were last modified May 15, 2026. Hourly wages are stored on the same object. The annual formula does not use them.
Benefits are the ECEC June 2026 row for private industry workers in Management, professional, and related occupations. Release USDL-26-1494, released September 9, 2026. Total compensation is $78.88 per hour. Wages and salaries are $54.06 per hour. Benefits are $24.82 per hour, 31.5 percent of total compensation. The formula uses the dollar row, not that rounded percent. The same formulas are on the fractional CTO cost calculator.
export function benefitsLoadFactor(
totalCompensationPerHour = ECEC.totalCompensationPerHour,
wagesAndSalariesPerHour = ECEC.wagesAndSalariesPerHour,
): number {
return totalCompensationPerHour / wagesAndSalariesPerHour
}
export function fullyLoadedAnnual(annualWage: number, load = benefitsLoadFactor()): number {
return annualWage * load
}
export function timeFraction(input: Pick<FractionalCtoInputs, "timeUnit" | "daysPerWeek" | "hoursPerWeek">): number {
if (input.timeUnit === "hours") {
return finiteOrZero(input.hoursPerWeek) / FULL_TIME_HOURS_PER_WEEK
}
return finiteOrZero(input.daysPerWeek) / FULL_TIME_DAYS_PER_WEEK
}
Fully loaded annual cost equals the annual wage times 78.88 divided by 54.06. The time-fraction cost equals that loaded cost times the time fraction. Days use days per week divided by 5. Hours use hours per week divided by 40. Forty is 2,080 divided by 52, the OEWS year-round full-time hours. The monthly figure is the annual time-fraction cost divided by 12. Displayed dollars are rounded to the nearest dollar from the unrounded formula. They are not recomputed from the rounded lines.
Revenue picks which published band is shown. BLS does not publish these wages by revenue. The mapping is an assumption, and the tool says so. Under $50 million uses the annual mean through the 75th percentile. $50 million to $250 million uses the 75th percentile through the 90th percentile. $250 million or more uses the 90th percentile only.
A monthly quote, when the caller supplies one, becomes an annual quote by multiplying by 12. The difference is the fully loaded full-time cost minus that annual quote.
How is the inputSchema designed so an agent fills it correctly?
required is only revenue_band. Days and hours stay optional properties, and each description tells the agent to send one of them. Each time field says a full-time week is 5 days or 40 hours. The hours description says that if both are set, hours_per_week is used. quoted_monthly_rate is optional, minimum 0, and the description says it is not a Pineapples rate.
The band is an enum, not free text.
const REVENUE_BAND_ENUM = ["under-50m", "50m-to-250m", "250m-or-more"] as const
revenue_band: {
type: "string",
enum: [...REVENUE_BAND_ENUM],
description:
"Company revenue band. under-50m is under $50 million, 50m-to-250m is $50 million to $250 million, and 250m-or-more is $250 million or more. The band selects a published wage range. That mapping is an assumption.",
},
An agent that follows the schema sends one of those three ids. The wrapper also accepts the human labels and the camelCase keys (revenueBand, daysPerWeek, hoursPerWeek, quotedMonthlyRate). resolveBand trims, lowercases, collapses whitespace, and matches the id or the label. That is tolerance for a messy caller. The contract is the enum.
function readNumber(value: unknown): ReadNumber {
if (value === undefined || value === null || value === "") return { state: "absent" }
const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : Number.NaN
if (!Number.isFinite(numeric) || numeric < 0) return { state: "invalid" }
return { state: "ok", value: numeric }
}
Absent means the field was left out. Invalid means it was present and not a finite number of zero or more. If days or hours are invalid, the tool calculates nothing and says both fields must be numbers of zero or more. If both are absent, it calculates nothing and says to provide days_per_week or hours_per_week. If the band does not resolve, it lists the three ids with their labels and calculates nothing.
Hours win when both are usable.
const hours = readNumber(input.hours_per_week ?? input.hoursPerWeek)
const days = readNumber(input.days_per_week ?? input.daysPerWeek)
const quote = readNumber(input.quoted_monthly_rate ?? input.quotedMonthlyRate)
const timeUnit = hours.state === "ok" ? "hours" : "days"
There is no scope property and no engagement property. Sending one does nothing to the dollars, because the wrapper never reads it. An invalid quote does not wipe the estimate. The range still returns, with one extra sentence that the quote was not used.
Why does the tool never quote a Pineapples price?
The tool description is explicit: "Not a Pineapples rate and not a quote." The wrapper comment says the dollars come from the BLS figures and the wrapper does not add a Pineapples price. The assumptions line in every successful response says "This is not a Pineapples rate." The last line is "Estimate only. Not a quote."
quoted_monthly_rate is the caller's number. Leave it out and quotedMonthlyRate stays null, quoteAnnual stays null, and the annual difference stays off the text. The module has no Pineapples rate to substitute. The business case for the seat is a different page, Fractional CTO Rates: What the Seat Returns. This tool does not read a rate from that page.
How did we test it?
lib/webmcp/estimate-fractional-cto.test.mjs has 3 tests. lib/fractional-cto-cost.test.mjs has 10. Those two files hold 13 tests. A run of both files reports 13 passed and 0 failed.
The 3 wrapper tests check the text. The first calls revenue_band 50m-to-250m and days_per_week 2, with no quote. It requires the full-time range, the time-fraction cost, the monthly range, both source lines, May 2025, June 2026, September 9, 2026, the word assumption, the calculator URL, and "not a Pineapples rate". It requires that "Annual difference" is absent. It then takes every dollar amount in the text, after removing the band labels "$50 million" and "$250 million", and requires each amount to be one of the six formatted figures from calculateFractionalCtoCost.
The second test sends the label "Under $50 million", days_per_week 2, hours_per_week 8, and quoted_monthly_rate 10000. The text must say "8 hours per week" and must not say "2 days per week". It must include the annual difference for that caller number. 10000 is the input the test passes. It is not a Pineapples rate.
The third test drops the band, then drops the time. Both replies are the error sentences, and neither contains a dollar amount.
The 10 calculator tests keep the May 2025 OEWS wages for SOC 11-3021 and the June 2026 ECEC row, check the load ratio, check a 2 day week against the 75th to 90th band, map the smaller and larger bands, convert 20 hours as half of a 40 hour week, compare a monthly quote of 10000 on an annual basis, and check that a different scope and engagement do not change the dollars. Two of the 10 read source files. The calculator intro stays between 40 and 60 words. The module, the calculator component, and the page contain no em dash, no en dash, and no Pineapples price token.
The default dollar assertion is this pair:
assert.equal(formatDollarRange(result.loadedAnnualLow, result.loadedAnnualHigh), "$322,071 to $434,103")
assert.equal(formatDollarRange(result.fractionalAnnualLow, result.fractionalAnnualHigh), "$128,829 to $173,641")
What does the same default as the live calculator return?
DEFAULT_INPUTS opens the calculator at revenue band 50m-to-250m, scope architecture and vendor oversight, engagement retainer, time unit days, 2 days per week, an hours field of 16, and no monthly quote. The screen uses days, so the hours field is not in the fraction. The matching tool call is revenue_band 50m-to-250m and days_per_week 2.
The band is the 75th percentile, $220,730, through the 90th percentile, $297,510. 220,730 times 78.88 divided by 54.06 rounds to $322,071. 297,510 on the same load rounds to $434,103. 2 divided by 5 is 0.4. Times the loaded band, the year is $128,829 to $173,641. That unrounded year divided by 12 rounds to $10,736 to $14,470 a month.
The hours field starts at 16, which is an assumption on the form, not a BLS wage. 16 divided by 40 is also 0.4. If an agent sends both defaults, hours win and the sentence changes to hours, and the dollars stay the same only because 16 and 2 days are the same fraction. Send 2 days and 8 hours, and the dollars follow 8 hours. The second wrapper test is that case.
This is the text the function returns for the calculator default:
Revenue band: $50 million to $250 million.
Full-time loaded range: $322,071 to $434,103 a year (75th percentile to 90th percentile).
Time-fraction cost: $128,829 to $173,641 a year ($10,736 to $14,470 a month).
Time fraction: 0.4 (2 days per week divided by 5).
Sources: Source: U.S. Bureau of Labor Statistics, OEWS May 2025. SOC 11-3021, Computer and Information Systems Managers, National. Data date May 2025. Tables last modified May 15, 2026. https://data.bls.gov/oesprofile/?year=2025&major_group=110000&occupation=113021&measure=01&areas=INDUSTRY,STATE,MSA
Source: U.S. Bureau of Labor Statistics, Employer Costs for Employee Compensation, June 2026 (released September 9, 2026). Table 4. Employer Costs for Employee Compensation for private industry workers by occupational and industry group. Release USDL-26-1494. Benefits are 31.5% of total compensation for Management, professional, and related occupations. The load uses 78.88 divided by 54.06. https://www.bls.gov/news.release/ecec.htm
Assumptions: Company revenue selects which published OEWS wage band is shown. BLS does not publish these wages by revenue. Under $50 million uses the annual mean through the 75th percentile. $50 million to $250 million uses the 75th percentile through the 90th percentile. $250 million or more uses the 90th percentile only. That mapping is an assumption. A full-time week is 5 days in the days formula and 40 hours in the hours formula (2,080 hours divided by 52 weeks). If both days_per_week and hours_per_week are provided, hours_per_week is used. Scope and engagement do not change the dollars. Displayed dollars are rounded to the nearest dollar from the unrounded formula. This is not a Pineapples rate.
Calculator: https://pineapples.dev/tools/fractional-cto-cost-calculator
Estimate only. Not a quote.
How can a CFO or operating partner use it through an AI assistant?
Use a browser on pineapples.dev where the assistant can call the tool. Give it the revenue band enum and either days_per_week or hours_per_week. Leave quoted_monthly_rate off unless you already have a monthly figure from a proposal you are holding. Read the full-time loaded range and the time-fraction cost. Those are the BLS figures. If you pass the quote, read the annual difference as the full-time loaded cost minus that quote times 12. Positive means full-time is higher.
The same math is on the fractional CTO cost calculator, if you want the formulas on a page instead of in a tool result. The range is not a decision to hire. The seat, separate from this range, is Fractional CTO Rates: What the Seat Returns. If the open question is who owns the AI calls, that is a fractional chief AI officer decision, not a script.
Frequently asked questions
Does estimate_fractional_cto_cost quote a Pineapples price?
No. The dollars are the BLS loaded range and the time-fraction of that range. A quoted_monthly_rate is a number the caller supplies, used only for the annual difference against the full-time loaded cost. The tool description says it is not a Pineapples rate and not a quote.
What does the default call return?
At revenue band 50m-to-250m and 2 days per week, with no monthly quote, the full-time loaded range is $322,071 to $434,103 a year. The time-fraction cost is $128,829 to $173,641 a year, which is $10,736 to $14,470 a month. That matches the calculator default.
Which field wins when both days and hours are set?
hours_per_week wins. The schema says so, and the wrapper sets the time unit to hours when that field is a number of zero or more. A full-time week in the hours formula is 40 hours.
Is there a usage count for this tool?
No. execute pushes a webmcp_tool_call event when dataLayer exists, with the tool name and the page path. If dataLayer is missing, the push is skipped. There is no count to report.
Working a live deal?
Book a 30-minute working session.
Same operator who runs the diligence engagements. No SDRs, no sales team. Bring the target, I'll bring the checklist.
Share this article

Anthony Wentzel
Founder, Pineapples
Anthony Wentzel has spent 26 years helping mid-market, PE, and family-office operators turn technology risk into decisions they can own. He is the founder of Pineapples.