For agents · Model Context Protocol
Connect an assistant to billoquy
mcp.billoquy.com is an MCP server. Point an MCP-capable assistant at it and, on a signed-in person's behalf, it can discover the checks, start one, read the finding with cited sources, preview a prepared fix, and hand the person off to approve and pay. It cannot approve, file, send, or pay — those are the person's actions.
Connect
The server is a streamable-HTTP MCP endpoint. Configure your client with the endpoint and the person's agent key as a bearer token:
| Server name | billoquy (v1.0.0) |
|---|---|
| Endpoint | https://mcp.billoquy.com/mcp |
| Auth header | Authorization: Bearer <agent key> |
A typical client config looks like this:
{
"mcpServers": {
"billoquy": {
"url": "https://mcp.billoquy.com/mcp",
"headers": { "Authorization": "Bearer <agent key>" }
}
}
}list_checks and the two resources need no auth, so a client can discover the surface before a key is attached. Every tool that touches an account needs the key. Clients that cannot send headers may instead pass the key as an accessToken argument on each account-scoped tool.
Auth: the agent key
An agent key is a billoquy session bound to a person's account with the purposeagent. It acts on that person's behalf: it is owner-scoped to their account and nothing else. It is the same token model the web app uses, with one added purpose — there is no parallel auth scheme.
- Minted by the person, human-only. The account owner creates and names a key in the AI assistant keys panel on their account page. Minting is a human action — an agent cannot mint, list or revoke its own credentials. The key is shownonce and never readable again.
- Revoked by the person. The same panel lists every key the account holds by label and revokes any of them, including one whose value the owner no longer has. Developers have the same three routes directly: see the developer API.
- Fails cleanly when absent. An account-scoped tool called without a key returns an agent-legible error pointing to where the person creates one — never a silent or misleading result.
The approval guardrail
An agent can prepare — only the human approves
An agent connected to billoquy can discover checks, start them, read findings, preview a prepared fix, and hand the person a link to approve or pay. It can neverapprove, file, send, deliver, or pay. This is not a convention — it holds at three layers:
1. The edge (the real gate). Approving or declining a prepared fix requires ahuman token. An agent key — owner-scoped though it is — gets a 403 at the approval endpoint, no matter what any client does.
2. The MCP surface. There is simply no approve, decline, file, send, or pay tool. The client the tools are built on has no such method, so it cannot reach that endpoint.
3. Preview, then hand off. get_prepared_fix returns the drafted artifact and an approval link for the person to open in their own signed-in session — nothing more.
Tools
Seven tools. Only list_checks is unauthenticated.
| Tool | Auth | What it does |
|---|---|---|
list_checks | none | Discover the checks and the approval policy. |
start_check | agent key | Start a free check for the owner. |
get_check | agent key | Poll/read a check and its finding. |
get_prepared_fix | agent key | Preview a prepared fix + get the approval link. |
start_fix_checkout | agent key | Get a Stripe Checkout link to hand the person. |
get_billing_portal | agent key | Get the self-serve billing portal link. |
list_account | agent key | List the account's checks, fixes, and receipts. |
list_checks
No input, no auth. Returns { checks, approvalPolicy }. Each entry inchecks carries id, name, tagline, available, finds, record, recordUrl, worth, input[], requiresDocument, documentHint, preparesFix and, when not available yet, an availabilityNote. preparesFix is what says whetherstart_fix_checkout can get anywhere for that check at all.
→ list_checks {}
← {
"checks": [
{
"id": "energy",
"name": "Energy Check",
"available": true,
"finds": "the wrong rate plan",
"record": "public utility rate database",
"recordUrl": "https://apps.openei.org/USURDB/",
"worth": "$200–600/yr",
"input": [
{
"name": "zip",
"required": true,
"description": "5-digit US ZIP code"
}
],
"requiresDocument": true,
"documentHint": "a photo or PDF of a recent utility bill",
"preparesFix": false
}
// ...one per check
],
"approvalPolicy": "billoquy prepares; the person approves. ..."
}start_check
Inputs: product (an id from list_checks), intake (the structured fields for that check — see per-check intake), and for document-centric checks either documentUrl (a public https link to a PDF or photo) or uploadKey(already staged with billoquy). Optional accessToken.
- Where a check can't yet answer for the person's input — an area whose public data isn't loaded — the diagnostic resolves to
status: "unsupported"onget_check, carrying the reason — nothing is charged. - A document check with neither
documentUrlnoruploadKeyreturns an error telling you to pass one (or have the person upload it). - On success:
{ status, diagnosticId, next }. Pollget_checkwith the id.
→ start_check {
"product": "energy",
"intake": { "zip": "78704" },
"documentUrl": "https://example.com/my-utility-bill.pdf"
}
← { "status": "received", "diagnosticId": "diag_…", "next": "Poll get_check …" }get_check
Input: diagnosticId (+ optional accessToken). The response depends on status:
- complete:
{ status, product, headline, detail, dollars, confidence, worthFixing, citations }, wheredollarsis{ amount, basis: "annual" | "one-time", currency: "USD" }ornull, and each citation is{ label, source, url }. WhenworthFixingisfalseand the finding still carries dollars, afixNotOfferedobject says which refusal applies —informational_check(this check sells no fix at any figure) orbelow_fix_floor(it sells one, but not for a finding this small). - unsupported:
{ status, product, message }. - failed:
{ status, product, reason }. - otherwise (still running):
{ status, product, note }— poll again shortly.
→ get_check { "diagnosticId": "diag_…" }
← {
"status": "complete",
"product": "energy",
"headline": "You're on the wrong rate plan",
"detail": "Switching to Plan B saves about …",
"dollars": { "amount": 340, "basis": "annual", "currency": "USD" },
"confidence": "high",
"worthFixing": false,
"fixNotOffered": {
"reason": "informational_check",
"note": "The Energy Check reports what it found and stops there — …"
},
"citations": [
{ "label": "Utility tariff", "source": "OpenEI USURDB", "url": "https://apps.openei.org/USURDB/" }
]
}get_prepared_fix
Input: diagnosticId (+ optional accessToken). If a fix has been drafted (the person paid), returns{ prepared: true, title, fixStatus, artifactPreview, approval } whereapproval is { required: true, by: "human", url, note } — the link the person opens to approve. If not, returns{ prepared: false, message, diagnosticPage }. This tool never approves.
start_fix_checkout
Input: diagnosticId (+ optional accessToken). Returns{ checkoutUrl, note } — a Stripe Checkout link to hand the person. The agent cannot pay; the person completes payment themselves, after which billoquy drafts the fix and it waits for their approval. Refused where there is no fix on offer: a check whosepreparesFix is false sells none at any figure, and a check that does still declines below the fix floor. Read worthFixing on get_checkfirst and relay the fixNotOffered reason rather than sending someone to a link that will not open.
get_billing_portal
Input: optional accessToken. Returns { portalUrl, note } — the self-serve Stripe billing portal link for the person. No money moves through the agent.
list_account
Input: optional accessToken. Owner-scoped summary:{ user, diagnostics, preparedFixes, receipts } — diagnostics with status and finding headline, prepared fixes with their approval status (approvedVia), and receipts (whatWeDid, whatItMeans, whatHappensNext).
Per-check intake
The intake object for start_check, per product. Calllist_checks for the authoritative, machine-readable version.
| product | intake fields | document | available |
|---|---|---|---|
energy | zip (5-digit US ZIP code) | Yes — a utility bill | yes |
fees | (none) | Yes — a bank or card statement | yes |
Resources
Two read-only resources describe the surface without any tool call:
billoquy://checks— the checks as JSON (application/json): what each finds, its public record, inputs, and availability. The same datalist_checksreturns.billoquy://approval-policy— the "we prepare, you approve" guarantee in prose (text/plain): what an agent can and cannot do.
A worked example
Running a Fee Check for the connected person and handing them the payment and approval links:
- Connect with the person's agent key as a bearer token.
list_checks→ confirmfeesis available, needs a document, and haspreparesFix: true.start_checkwith{ product: "fees", intake: {}, documentUrl }→ adiagnosticId.get_checkon that id untilstatus: "complete"→ the finding, withdollarsand citations.- If
worthFixing:start_fix_checkout→ hand the personcheckoutUrlto pay. If not, relayfixNotOffered.note— the finding is theirs to act on, free. - After they pay:
get_prepared_fix→ hand themapproval.url. They approve on the web; the agent never does.
Honest limits
- Availability. A check's coverage can be built out area by area, so a started check may come back from
get_checkasunsupportedwith amessagesaying so. Relay it — do not lead the person into a dead end. - Polling.
get_checkis a single fetch; there is no server-side long-poll. Poll it until the status is terminal. - Documents.
documentUrlmust be a publicly fetchablehttps link (it is fetched under strict SSRF guards). Documents behind a login must be uploaded by the person; the tool's error copy says so. - Prefer raw HTTP? The MCP is a pure client of the same edge API — see thedeveloper API reference.