# mcpbelt > Multimodal capabilities for AI agents over MCP: read documents, transcribe audio. One connection, one prepaid balance, no per-provider API keys. Short version, with the price list and the connection command: https://mcpbelt.com/llms.txt Machine-readable price list: https://mcpbelt.com/pricing.json This file is every problem page on the site, in full. There are 20. Each one also exists on its own at the address shown above it. --- # Reading documents ## Your agent cannot read a scanned PDF > A scan is a picture of a page. There is no text to extract, so the file opens fine and comes back empty. Source: https://mcpbelt.com/problems/read-a-scanned-pdf ### The problem You hand your agent a PDF. It opens, it has forty pages, and the answer that comes back is that the document appears to be empty, or worse, a confident summary of the two lines that happened to be in the file's metadata. The file is a scan. Someone put paper on a glass plate, and what came out is a picture of a page wrapped in a PDF container. To a human it looks identical to a document. To anything that reads text, it is a photograph with nothing to read. This is the single most common way a document workflow fails, and it fails silently: no error, no warning, just an answer built on nothing. ### Why it fails locally A PDF is a container, not a format. It can hold a text layer, or page images, or both. `pdftotext`, `pypdf` and every library like them read the text layer, and on a scan that layer does not exist. They return an empty string and no error, because from their point of view nothing went wrong. Recognising the characters in the image is optical character recognition, and that is a different job from parsing a file. Running it locally means installing an OCR engine, keeping its language data current, and accepting whatever quality it gives you. The part nobody mentions is the hard part: **you cannot tell when it read badly.** An engine that gets 15% of the characters wrong returns the same shape of answer as one that got them all right. So the naive local path either returns nothing, or returns something wrong with no way to know which. ### The code Upload the file, then read it. The `key` comes from `upload` and is not a path on your disk: this service cannot see your disk. ``` upload(filename="invoice.pdf", content_type="application/pdf") -> key, upload_url curl -T invoice.pdf "" read(key="") -> handle, engine, cost_eur, structure, text ``` ### What it costs `read` in `balanced` mode costs 0.007 EUR per page. It first tries to extract the text locally, which costs nothing, and pays an OCR engine only when that fails. On a scan it always fails, so a scan always costs the per-page price. `exact` costs 0.016 EUR per page and skips the free attempt entirely. Use it when the PDF does carry a text layer but you do not trust it, which happens on old scans that were run through someone else's OCR years ago. Pages are counted from the file itself, not estimated from its size, so a forty-page scan is charged as forty pages. If the same file is read twice, the second call returns the first result and charges nothing. ### What this does not do - The text comes back as text. Handwriting, stamps and signatures are not reliably read by any engine, ours included. - Email files (EML, MSG) and ebooks (EPUB) are read by no engine. Extract the text or convert to PDF. - The file is fetched from the address you upload it to and passed to an engine that runs elsewhere. If the document cannot leave your machine, this is the wrong tool. --- ## Your agent turns a table into a stream of loose numbers > The grid is a property of the page, not of the text. Extract the text naively and the rows stop lining up with the columns. Source: https://mcpbelt.com/problems/keep-a-table-a-table ### The problem There is a table on page 12 and you asked for the figure in the third column. A figure comes back. It is a real figure from that table, in the wrong row, and nothing in the answer says so. What arrived was not a table. It was the values from the table, run together into a sequence: the headers, then the cells in whatever order they came out, with the one thing that carried the meaning gone. A cell means what it means because of the row and the column it sits in. Strip the grid and every number is just a number. This is the failure mode that gets past review, because there is nothing to review. The answer is fluent, the number is real, and the arithmetic your agent does with it afterwards is arithmetic on the wrong cell. ### Why it fails locally A PDF does not contain rows or columns. It contains glyphs, each with a position on the page, and a text extractor returns them in the order they were written into the file. That order is whatever the program that generated the PDF happened to emit, and it is not the order a person reads. A two-column page can come back with the two columns interleaved line by line. A table can come back column by column, so the first row of your output is the first column of the page. Turning those positions back into a grid means clustering coordinates: values that share an x range are a column, values that share a y range are a row. Every library that does this does it by heuristic, and the heuristics break on the things real tables are full of. A merged header cell. A value that wraps onto two lines. A column separated by whitespace instead of a ruled line. A footnote sitting between two rows. When the clustering guesses wrong it does not fail, it produces a different table, well formed and wrong. On a scanned table there are no coordinates to cluster in the first place. There is one image of a page, and the grid has to be recovered from pixels: that is character recognition and layout analysis at once, which is a model, not a parsing library. ### The code `exact` skips the local text extraction, which is the step that flattens the grid, and sends the pages to an engine. `pages` keeps the work to the range that has the table. Read `engine` and `used_fallback` in the response: they tell you which of the two engines answered, and that is what decides whether the grid survived. ``` upload(filename="report.pdf", content_type="application/pdf") -> key, upload_url curl -T report.pdf "" read(key="", mode="exact", pages="12-14") -> handle, engine, used_fallback, cost_eur, structure result(handle="", pages="12") -> that page, in reading order result(handle="", find="Total") -> excerpts, each with the page it falls on ``` ### What it costs `exact` costs 0.016 EUR per page and is the mode to ask for on a dense table. `balanced` costs 0.007 EUR per page and tries the local text layer first, which is free and which is exactly the path that loses the grid. On a table the free attempt is not a saving, it is the failure you came here to avoid. `pages` narrows the work to the range that matters, so a table in a two hundred page report is a read of three pages and not of two hundred. Pages are counted from the file itself, never estimated from its size, and `result` is free however many times you page back through what was read. The same file read twice is charged once: it is recognised by its content, not by its name. A read that fails is not charged at all. ### What this does not do - **Table structure is not part of what the mode guarantees.** Every mode has a contract that any engine in it must satisfy, and the grid is not in it: what is guaranteed is the text, and the page each line came from. The first engine reconstructs tables and the backup returns the page flat, at the same price. `engine` and `used_fallback` in the response say which one you got, on every call, and that is deliberate: a fallback you cannot see is a weaker result you paid full price for. - You get rows of text, not a spreadsheet. There is no cell you can address by coordinates, and no CSV. - `structure` reports what was found, and `structure_source` says whether that structure was read from the document or inferred from the text. Inferred structure is a good guess about where a section starts, nothing stronger. - A table split across a page break comes back as two tables, one per page. Rejoining them is your side of the work. - A spreadsheet is a file whose table is already a table: read it as one, without paying anyone. This page is for the case where all you have is the PDF. --- ## Every supplier lays out an invoice differently > The fields are always the same five. Where they sit on the page never is. Read the invoice properly, then let the model pull the fields out of the text. Source: https://mcpbelt.com/problems/extract-fields-from-an-invoice ### The problem You need the invoice number, the date, the net amount, the total and the VAT number. Five fields, the same five every time, and forty suppliers who each put them somewhere else. One prints the number in the header, one in a boxed panel on the right, one only in the payment stub at the bottom. One writes `Invoice no.`, one writes `Doc.`, one writes nothing at all and just prints the number under the logo. Net and total change places depending on whether the tax lines are itemised. A third of them arrive as scans, because someone printed the PDF, signed it and put it back on the glass. The rule you wrote against the first ten works on the first ten. On the eleventh it does not come back empty, which you would notice. It comes back with a number that is on the page and is not the total. ### Why it fails locally A rule that looks for the label `Invoice No.` and takes what follows it is fitted to one layout. The next supplier writes the label on its own line with the value underneath, and the rule takes the word that follows the label, which is now the date. You end up with one rule per supplier, and a supplier changes its billing template without telling you. The deeper problem is that the label is not next to its value in the text, it is next to it on the page. Extraction returns characters in the order they were written into the file, so a label in a top-right panel and the value printed beneath it can be hundreds of characters apart in the string, or in the opposite order. Rules that count on proximity are counting on an accident of how the generator emitted the file, and that accident is different for every supplier. And when the invoice is a scan there is no text to run a rule over at all. The pattern matches nothing, the field comes back empty, and an empty field is indistinguishable from a field that was genuinely not on the invoice. ### The code Two steps, and the second one is not a tool call. `read` returns the text with the structure intact. Pulling five fields out of readable text is what a model is already good at, so that part is yours. ``` upload(filename="invoice-0141.pdf", content_type="application/pdf") -> key, upload_url curl -T invoice-0141.pdf "" read(key="", mode="exact") -> handle, engine, used_fallback, cost_eur, structure, text # an invoice is a page or two, so `text` comes back whole # and the model reads the five fields out of it result(handle="", find="VAT") -> excerpts, each with the page it falls on ``` ### What it costs `exact` costs 0.016 EUR per page and goes straight to an engine, so a two page invoice is 0.032 EUR. It is the mode to use when the layout carries the meaning, which on an invoice it always does. `balanced` costs 0.007 EUR per page and tries the local text layer first. That attempt is free, so on a run of suppliers where some send machine-generated PDFs and some send scans, you pay only for the scans. The same invoice read twice is charged once. The file is recognised by its content and not by its name, so the copy your accounts inbox renamed is still the same document and does not cost a second time. A read that fails is not charged at all. ### What this does not do - This returns text, not fields. There is no invoice schema, no field names, no JSON of the document: the five values are extracted by the model from the text, and that is the step you check. - If a single page of the document fails the quality check, the whole document goes to an OCR engine. A check on the document average would hand back the blank pages at full price. - Email files (EML, MSG) and ebooks (EPUB) are read by no engine. Extract the text or convert to PDF. - An email with the invoice attached is not a document. Pull the attachment out first, then upload the attachment. --- ## The thing you need to read is a screenshot, not a document > Your client can look at an image. Reading a dense page end to end, in order, without dropping a line is a different operation. Source: https://mcpbelt.com/problems/read-a-screenshot-or-a-photo-of-a-page ### The problem What you have is a screenshot of a stack trace, or a photo of a page someone took with a phone: a little rotated, the shadow of a hand across the top, the far margin curving into the gutter. It is not a document. There is no text layer to fail on, no pages, no container. There is an image, and the words you need are drawn inside it. Every tool that expects a file to parse has nothing to work with. Most clients will accept the image and answer about it, which is why this failure is easy to miss. On a page with sixty lines of small print the answer comes back confident, fluent, and built on the lines that were noticed. ### Why it fails locally Looking at an image and reading it are two different jobs. A multimodal model takes in the whole frame and answers about it, which is the right operation for a diagram, a chart or a photograph of a room. Transcribing every character in reading order is not that operation. On a dense page the model works from what it attended to, attention is not exhaustive, and a line it did not attend to leaves no trace in the output. There is no gap, no ellipsis, no note that something was skipped: the sentences around the hole simply close over it. Doing it properly yourself means an OCR engine, and on a phone photo it means everything that happens before the OCR engine. A page shot at an angle has text lines that converge instead of running parallel, so the line-finding step pairs up rows that belong to different lines and returns interleaved fragments. Shadow across the top half changes the threshold that separates ink from paper, and a fixed threshold turns the shadowed part into either solid black or blank white. This is the part that sounds like installing a package and is not. Be honest about which case you are in. If the image is a chart, a screenshot of a user interface you want described, a whiteboard sketch or a photo of a scene, your client's own vision does that well and does it for free. This page is for the other case: the image is a page of text, and you need all of it, in order, with nothing quietly missing. ### The code An image is uploaded and read exactly like any other file. Declaring `size_bytes` gets an oversized upload refused before it happens rather than after. ``` upload(filename="ledger-page.jpg", content_type="image/jpeg", size_bytes=2411008) -> key, upload_url curl -T ledger-page.jpg "" read(key="", mode="exact") -> handle, engine, used_fallback, cost_eur, structure, text result(handle="", find="ORA-01017") -> excerpts, each with where it falls ``` ### What it costs One image is one page. `exact` costs 0.016 EUR and goes straight to an engine: it is the mode for a photograph, where the reading is the hard part. `balanced` costs 0.007 EUR and tries local extraction first, which on an image finds nothing and then pays anyway, so the free path never saves you anything here. Units are counted from the file, not estimated from its weight, so a single screenshot is charged as a single page whatever it happens to weigh. The same image read twice is charged once: files are recognised by their content, not by their name, which matters when every screenshot on the machine is called `Screenshot 2026-08-08 at 10.14.21`. A read that fails is not charged. Every response names the engine that produced it in `engine` and reports in `used_fallback` whether the backup answered: same price, same guarantees, and never a quieter result sold as the full one. ### What this does not do - Handwriting, stamps and signatures are not reliably read by any engine, this one included. - The frame has to contain the page. A photo cropped mid-sentence gives back a cropped sentence, and nothing reconstructs what was outside the shot. - You get the text, not a description of the picture. If the question is what the chart shows or what is happening in the photo, that is your client's own vision, and it is free. - The image is uploaded and handed to an engine that runs elsewhere. If it cannot leave your machine, this is the wrong tool. --- ## An eighty-page document does not fit in your agent's head > Eighty pages returned into a conversation are about twenty thousand tokens that never leave again, and they degrade every answer that comes after. Source: https://mcpbelt.com/problems/read-an-eighty-page-document ### The problem The document is eighty pages and somebody needs it understood. The obvious move is to read it and hand the text to the model, and the obvious move is the one that ruins the conversation. Eighty pages of text are roughly twenty thousand tokens. That is not a one-off cost. Once they are in the conversation they stay there, they are re-sent with every following turn, and they push out the things your agent actually needs to remember. The answer about the document gets worse, and so does the next answer about something else entirely. The part that makes it hard to notice is that nothing fails. The tool call succeeds, the text is correct, the summary is plausible. What degrades is everything after, and there is no error message for that. ### Why it fails locally Reading the file yourself gives you the whole thing at once, and that is the problem: extraction hands you one string and no place to put it. Whatever you do next, you have already paid the context for all of it. The fixes people write are worse than they look. Truncating to the first N characters answers questions about the first chapter and lies about the rest. Chunking and summarising each chunk means one model call per chunk, and the summary is now a lossy copy you cannot search: the clause you needed was in the sentence that got compressed away. The real requirement is to keep the full text somewhere the model can query without carrying it, and to know how much context you dodged. **Neither of those exists in a local extraction call.** It returns a string, and a string has no handle, no page map and no token count. ### The code `read` returns a preview, a map and a handle, not the text. Then you ask for the part you need. `result` is free, however many times you call it. ``` upload(filename="report.pdf", content_type="application/pdf") -> key, upload_url curl -T report.pdf "" read(key="") -> handle, engine, structure, chars, tokens_if_inlined, truncated result(handle="", outline=true) -> section titles, each with its page result(handle="", pages="12-14") -> just those pages result(handle="", offset=0) -> a window of text, plus next_offset to continue ``` ### What it costs `read` in `balanced` mode costs 0.007 EUR per page, so eighty pages are about 0.56 EUR. A PDF that already carries its own text is extracted locally and costs nothing at all: you pay only when an engine has to look at the pages. `exact` costs 0.016 EUR per page and skips the free attempt. Pages are counted from the file, not estimated from its size, so eighty pages are charged as eighty pages and not as a guess. The context saving is measured, not claimed: on a forty-page document the response was 459 tokens instead of the 6,355 it would have taken inlined. The `tokens_if_inlined` field tells you, on your own file, what you did not spend. Every call to `result` after that is free. Outline, page ranges, searches, windows: the work was paid for when it ran, and querying it costs nothing. ### What this does not do - The full text is kept for a limited time. When it expires the handle stops answering and reading the file again is a new job at the normal price. - Email files (EML, MSG) and ebooks (EPUB) are read by no engine. Extract the text or convert to PDF. - `result` will not hand you the whole document back in one call. That is the point of it: if you genuinely need all eighty pages in context, this page is not solving your problem. - Section titles come from the document when the engine reports them and are inferred from the text otherwise. `structure_source` says which one you got. --- ## You need one clause out of an eighty-page contract > Search the result instead of reading it: excerpts come back with the page they fall on, and reading the pages around them is free. Source: https://mcpbelt.com/problems/find-one-clause-in-a-long-contract ### The problem Somebody asks what the termination terms are. The contract is eighty pages, the answer is two paragraphs, and nobody knows which page they are on. The two ways to get there are both bad. Read the whole thing into the conversation and you have spent about twenty thousand tokens to use two hundred words, and the other seventy-nine pages stay in context degrading everything that follows. Guess a page range and you are betting on where a clause lives in a document you have not read. What you want is the search you would do in a PDF viewer: type the word, get the hits with their page numbers, read around the ones that matter. That is the operation, and it is missing from every naive pipeline. ### Why it fails locally On a digital PDF, text search works and is not the hard part. On a scanned contract it finds nothing, because there is no text to search: the pages are pictures, and the empty result looks exactly like a document that does not mention termination. That is the wrong answer arriving with confidence. Even once the characters are recognised, extraction hands back one flat string. **The page numbers are gone.** You can find the word and still not be able to say where it is, and a clause without a page reference is not something anyone can check against the paper copy. So the local path either searches nothing, or searches something that has lost the one piece of metadata that makes the answer usable. And the workaround, reading everything so the model can find it, is precisely the cost you were trying to avoid. ### The code Read once, then search the result. `find` is a case-insensitive text search and returns excerpts with the page each one falls on. Every `result` call is free. ``` upload(filename="contract.pdf", content_type="application/pdf") -> key, upload_url curl -T contract.pdf "" read(key="") -> handle, engine, structure, tokens_if_inlined result(handle="", find="termination") -> matches, match_count, matches_omitted result(handle="", pages="12-14") -> the pages around the match, in full result(handle="", find="notice period") -> another search, same handle, still free ``` ### What it costs You pay once, for the read. `balanced` costs 0.007 EUR per page, about 0.56 EUR for eighty pages, and nothing at all when the PDF already carries its own text and can be extracted locally. `exact` costs 0.016 EUR per page and goes straight to an engine, which is what you want on a scan of a signed copy. Searching is free and so is paging. Ten searches and four page ranges cost the same as one: `result` is never charged, because the work was already paid for when the document was read. Reading the same contract twice costs once. The file is recognised by its contents rather than its name, so the second call returns the first result and charges nothing, even if somebody renamed it in the meantime. ### What this does not do - `find` is a text search, case-insensitive and literal. It matches words, not meanings: a clause that never uses the word you searched for will not come back. - A search that hits hundreds of times returns the first excerpts and reports the rest in `matches_omitted`, rather than flooding your context. Narrow the term or read the pages. - Page numbers are the pages of the file. If the printed document is numbered differently, the two will not agree. - Email files (EML, MSG) and ebooks (EPUB) are read by no engine. Extract the text or convert to PDF first. --- ## Five pages of a forty-page PDF come back empty > A mixed PDF passes every average-based check and still loses its scanned pages. Quality is checked page by page, and one bad page sends the whole file to OCR. Source: https://mcpbelt.com/problems/read-a-pdf-where-only-some-pages-are-scanned ### The problem The document is forty pages, generated by somebody's system, and in the middle of it there are five pages that were printed, signed and scanned back in. Annexes, a countersigned page, a form somebody filled in by hand and photographed. The file opens, the text comes out, the summary reads fine. Five pages are blank and nobody says so. Your agent answers questions about the contract without ever having seen the part that was signed. This is the same failure as a fully scanned PDF, except worse, because the document does not look broken. A scan that returns nothing is obvious within seconds. A document that returns thirty-five pages out of forty looks like a document that worked. ### Why it fails locally Every extraction pipeline has a moment where it decides whether the text layer is good enough, and almost all of them decide it with an average: characters per page across the file, or total characters over page count. Forty dense digital pages plus five empty ones produce a very high average. The check passes, extraction is declared successful, and the five pages are simply absent from the output. **An average over parts always hides the worst part.** It is not a tuning problem, it is the wrong shape of measurement: no threshold on a mean can tell forty-five good pages from forty good pages and five that are pictures. The signal you need is per page, and once you have thrown it away by averaging you cannot get it back downstream. The other half is what to do about it. Sending only the five bad pages to an engine sounds thrifty and produces a document assembled from two different readings, with two different notions of layout and reading order, and no record of which page came from where. ### The code Nothing special to ask for. The per-page check is in `balanced`, and the response says which engine did the work. ``` upload(filename="contract-with-annexes.pdf", content_type="application/pdf") -> key, upload_url curl -T contract-with-annexes.pdf "" read(key="") -> handle, engine, used_fallback, cost_eur, structure result(handle="", pages="18-22") -> the scanned annexes, as text like every other page ``` ### What it costs `balanced` tries local extraction first, which costs nothing, and checks the result page by page. If every page passes, the read is free. **If a single page fails, the whole document goes to an engine and the whole document is charged**: forty pages at 0.007 EUR each, about 0.28 EUR, for five pages you could not read. That is deliberate, and it is the honest version of the trade. Charging you for five pages and delivering a document read two different ways would be cheaper on the invoice and worse to work with, and you would have no way to tell which pages were which. `exact` costs 0.016 EUR per page and skips the free attempt entirely, which is what you want when you already know the file is mixed and would rather not pay for a local attempt that is going to be discarded. Pages are counted from the file, so a forty-page PDF is forty pages, and reading the same file again returns the first result and charges nothing. ### What this does not do - The rule is all or nothing on purpose: there is no option to OCR only the pages that failed. - Handwriting, stamps and signatures on those scanned annexes are not reliably read by any engine, ours included. What was signed comes back as an image nobody transcribed. - Email files (EML, MSG) and ebooks (EPUB) are read by no engine. Extract the text or convert to PDF. --- # Listening to audio ## Your agent cannot sit through an hour of recorded meeting > An hour of audio is not something a tool call can wait for. It gets queued, and you get a handle back immediately. Source: https://mcpbelt.com/problems/transcribe-an-hour-long-meeting ### The problem Someone recorded the meeting and wants the decisions out of it. The file is fifty-six minutes long, and every part of the obvious approach breaks. The model cannot listen to it: audio is not something most clients can put in front of a model, and the ones that can charge for the whole duration whether or not anything was said. A transcription API can, but an hour takes longer than any tool call is allowed to run, so the call times out and your agent retries, and now the same hour is being paid for twice. And when the text finally arrives it is one undivided block, which answers no question anyone actually has. Nobody wants the transcript. They want what was decided and who committed to it. ### Why it fails locally Running a speech model on your own machine is possible and it is slow: real time or worse on a laptop, so an hour of audio costs an hour of your machine. That would be tolerable if it ran in the background, but a tool call is not a background job. It has to answer. Speaker separation is the part that quietly does not work. Splitting a recording into who spoke when is a separate model from the one that turns sound into words, and running it well is not a matter of installing a package. Without it a four-person meeting comes back as one continuous voice, and every attribution your agent makes afterwards is a guess. There is also the arithmetic nobody does up front: an hour of transcript is roughly forty thousand tokens. Returning it into a conversation does not just cost that once. It sits in the context and degrades every answer that comes after it, including the ones about something else entirely. ### The code Audio longer than a few minutes is queued. `listen` returns immediately with a handle and `status: running`, and the same handle serves the result once it is ready. ``` upload(filename="meeting.m4a", content_type="audio/mp4") -> key, upload_url curl -T meeting.m4a "" listen(key="") -> handle, status: running, estimated_minutes result(handle="", outline=true) -> who spoke, and when result(handle="", find="deadline") -> excerpts, each with its timestamp result(handle="", time="9:30-11:00") -> that stretch, as turns of speech ``` ### What it costs `listen` in `balanced` mode costs 0.005 EUR per minute of audio, so a fifty-six minute meeting is about 0.28 EUR. `exact` costs 0.008 EUR per minute and uses the strongest model: worth it on poor recordings, crosstalk and heavy accents. Both modes separate speakers. That is not an upsell tier, it is part of what the mode guarantees, and an engine that cannot do it does not get used for this work. Duration is measured from the file, not estimated from its size, and a declared duration that disagrees with the file's weight is not trusted. You are charged once, on the real duration, whether or not you ever collect the result. Querying the transcript afterwards is free, however many times you do it: the work was paid for when it ran. ### What this does not do - Four hours is the ceiling on a single recording. Longer than that, split the file. - A queued job can be cancelled while it is still waiting and the credit comes back. Once an engine has started, it cannot: the money has already left. - Speaker labels are `Speaker A`, `Speaker B` and so on. Nothing identifies who those people are, and no voice is matched against any stored identity. --- ## The transcript is one wall of text with nobody's name on it > A transcript without speakers answers the wrong question. Speakers are separated in both modes, and the text is stored as turns of speech. Source: https://mcpbelt.com/problems/know-who-said-what ### The problem You have the transcript. Four people were in the room, and what comes back is one continuous paragraph in which somebody agrees to send the numbers by Friday. Which somebody is not written anywhere. That is the question nobody asked out loud but everybody wants answered. Not what was said: who said it. Who took the action, who objected, who committed to the date. A transcript that cannot attribute a sentence turns every conclusion your agent draws into an inference from word choice and turn order, and those inferences are confident and wrong often enough to be worse than nothing. It gets worse in the places it matters most. Two people talking over each other, a decision reversed twenty minutes later, someone speaking on behalf of a team: exactly the passages you need attributed are the ones a flat block of text destroys. ### Why it fails locally Splitting a recording into who spoke when is called diarization, and it is a **different model** from the one that turns sound into words. The speech model can be excellent and still hand you an undivided stream, because separating voices was never its job. Installing a transcription package does not get you this, and running the separate model well is not a matter of installing a second one: it needs the same audio, aligned against the word timings, with the number of speakers either known or estimated, and it degrades badly on crosstalk and on short interjections, which is where a meeting actually lives. The second half of the problem survives even when separation works, and it is the part people discover last. An engine hands back a single text field that does not say who is speaking, plus a list of segments that are roughly one sentence each. Neither is what you want to store. The text field has lost the attribution, and the segments have shredded a two-minute explanation into forty fragments, so a search that lands on one of them gives you a sentence with no argument around it. Recombining those fragments into turns of speech is a small piece of code that nobody writes until they have already stored a year of transcripts the other way. By then the fix is a migration. ### The code Speakers are separated by default, in both modes. `result` with `outline=true` returns who spoke and when, and it is free. ``` upload(filename="board-call.m4a", content_type="audio/mp4") -> key, upload_url curl -T board-call.m4a "" listen(key="", mode="exact", language="it") -> handle, engine, used_fallback, cost_eur, structure result(handle="", outline=true) -> who spoke, and when result(handle="", find="Friday") -> excerpts, each with its timestamp result(handle="", time="21:00-24:00") -> that stretch, as turns of speech ``` ### What it costs Attribution is not priced separately. `listen` costs 0.005 EUR per minute in `balanced` mode and 0.008 EUR in `exact`, and **both separate speakers**. There is no tier where you pay more to find out who talked. That is a guarantee of the mode, not a property of whichever engine happened to answer. An engine that cannot return speakers is not used for this work at all, and if the first engine fails the backup answers with the same guarantees at the same price. Every response tells you which engine ran, in `engine` and `used_fallback`. Reading the result afterwards is free, however you slice it: `outline` for the speaker map, `find` for a term, `time` for a stretch. You paid when the audio was transcribed, once, on its real duration. ### What this does not do - Labels are `Speaker A`, `Speaker B` and so on. Nothing identifies who those people are, and no voice is matched against any stored identity. - Heavy crosstalk is the hard case for every engine. `exact` handles it better than `balanced`, and neither is magic. - The transcript says who spoke, not what they meant. There is no summary, no sentiment and no judgement of who agreed with whom. --- ## You need the two minutes where the budget came up > Search the transcript for a term, get excerpts each with its own timestamp, then pull that stretch as turns of speech. Free, as often as you like. Source: https://mcpbelt.com/problems/find-the-minute-a-topic-came-up ### The problem The meeting was an hour. Somewhere in it the budget was discussed, a number was said out loud, and someone either agreed to it or did not. You need those two minutes. You do not need the other fifty-eight. Listening again is not a strategy. It costs an hour to find two minutes, and it costs that hour again the next time someone asks about the timeline instead of the budget. Scrubbing the waveform is worse: you are guessing at where a topic sits from the shape of the sound. So you transcribe it, and the problem moves rather than disappearing. Now you have forty thousand tokens of text and the same question, and the obvious move is to hand the whole transcript to the model and ask. That works once, expensively, and then the transcript is in the context for the rest of the conversation, quietly making every later answer worse. ### Why it fails locally An audio file cannot be searched. There is no index in it, nothing that maps the word budget to a position, and no way to look inside without decoding the sound into words first. Every approach that starts with the audio has transcription as its first step, whether you planned it or not. Once you have the text, the problem is a text problem and the audio was never the hard part. Locally that means holding the whole transcript somewhere and running your own search over it, which is easy, and then the two things that are not: mapping a character position back to a timestamp, and returning enough context around the hit to be worth reading. A one-sentence fragment that says `we can stretch to forty` tells you nothing about what was being stretched. This is where the segment shape a raw engine returns hurts most. Its segments are roughly one sentence each, so a search lands you inside an argument with no argument around it. What you want is the surrounding turns of speech, with who was speaking and at what minute, and that is a structure you have to build before you store anything. ### The code `find` returns excerpts with their timestamps, `time` returns that stretch as turns of speech. Both are free, however many times you call them. ``` listen(key="") -> handle, status: running, estimated_minutes result(handle="", find="budget") -> matches, each with its timestamp -> match_count, matches_omitted result(handle="", time="9:30-11:00") -> turns, duration result(handle="", time="1:02:00") -> the window around that point result(handle="", outline=true) -> who spoke, and when ``` ### What it costs You pay once, when the audio is transcribed: 0.005 EUR per minute in `balanced` mode, so an hour-long meeting is about 0.30 EUR. `exact` costs 0.008 EUR per minute. **Every later question about that recording is free.** `find`, `time`, `outline` and paging through with `offset` are not charged, now or in a week: the work was paid for when it ran, and querying it is reading, not redoing. Searching the same meeting thirty times costs the same as searching it once. It is also cheap in the other currency. Pulling the two minutes you asked for puts a few hundred tokens in your context instead of the forty thousand the whole hour would have cost, and the difference is not just money: the tokens you do not spend are the ones not degrading every answer after this one. ### What this does not do - `find` matches text, case-insensitive. It does not match meaning: a discussion about money that never says the word budget will not come back under that term. - `time` works on transcripts and `pages` on documents. Asking for a time range on a document is an error, not an empty answer. - A very common term returns the first matches and tells you how many it left out, in `matches_omitted`. Narrow the term rather than paging through all of them. - Results do not live forever. Once a result has expired, asking again means transcribing again, and that is charged. --- ## Someone sent a voice note instead of writing > Three minutes of speech, and your client cannot hear it. Short audio is not queued: the transcription comes back inside the same call. Source: https://mcpbelt.com/problems/transcribe-a-voice-note ### The problem Somebody recorded three minutes on their phone rather than typing four lines. It arrives as an `.m4a` in a chat, and it contains an address, a date and a change of plan, which is to say everything the next step depends on. Your agent cannot open it. Not because three minutes is a lot, but because audio is not something most clients can put in front of a model at all, and the file is the only place the information exists. Nobody wrote it down. That is the whole point of a voice note. This is a small problem that blocks a large one. The task is not transcription, it is booking the thing or answering the person, and it is stopped at the first step by three minutes of speech. ### Why it fails locally The client does not listen. Whatever is holding the conversation reads text, and a file it cannot decode is a file it cannot reason about: there is no partial answer here, no degraded mode, just a gap where the content was. Three minutes of talking is also not three minutes of clean dictation. It is speech: false starts, a sentence abandoned halfway and restarted, background noise from wherever the person was walking, and proper nouns. Names of people, streets and companies are the part that fails first and matters most, because a wrong name is not a typo, it is a wrong answer that reads as a right one. A small local model gets the easy sentences and mangles exactly those tokens. And running a speech model on your own machine is real time or worse on a laptop, so the cheapest case by price becomes one of the slower ones by wall clock, inside a tool call that has to answer. ### The code Short audio is not queued. `listen` transcribes it and returns the text in the same response, with no handle to poll. ``` upload(filename="note.m4a", content_type="audio/mp4") -> key, upload_url curl -T note.m4a "" listen(key="") -> handle, engine, cost_eur, chars, structure -> text # the whole transcription, inline # longer than a few minutes, the same call answers: # -> handle, status: running, estimated_minutes # and result(handle="") serves it once ready ``` ### What it costs This is the cheapest thing here. `listen` is priced per minute of audio, 0.005 EUR in `balanced` mode, so a three-minute note costs about 0.015 EUR. `exact` is 0.008 EUR per minute and is worth it when the recording is noisy or the names matter. The 0.10 EUR of trial credit covers a lot of voice notes before you have paid anything. Duration is measured from the file rather than estimated from its weight, and a duration declared in the file's header that disagrees with its size is not taken at face value, so a three-minute note is charged as three minutes. The same note sent twice is charged once: files are recognised by their content, not their name, and the second call returns the first result for nothing. If the transcription fails, nothing is charged at all. ### What this does not do - Under a few estimated minutes the text comes back inline. Above that the job is queued and you get a handle with `status: running` instead, which is the same tool behaving correctly, not a different one. - Speakers are separated here too, and on a one-person note that means a single `Speaker A`. Nothing identifies who that person is. - You get the transcription, not a reading of it. No summary, no translation, no tone. - Language is detected automatically. Pass `language="it"` when you already know it and the recording is poor: it removes a guess the engine would otherwise make. --- ## What matters is inside an MP4, and it is not the picture > A recorded call, a lecture, a demo. Everything worth having was said out loud, and you are charged for minutes of audio, not for the weight of the video. Source: https://mcpbelt.com/problems/transcribe-the-audio-of-a-video ### The problem Someone hands you the recording. It is an MP4: a Zoom or Meet call, a lecture, a customer demo, a screen share with a person talking over it. Everything anyone needs out of it was said out loud, and nobody is ever going to watch the picture again. The obvious move is to give the video to the model, and the obvious move does not exist. Most clients cannot put a video in front of a model at all, and a recording of any length is far too large to travel inside a tool call. What you actually want is much smaller than the file: the words, in order, with who said them and when. So the video has to become audio, and the audio has to become text. Two steps stand between your agent and the one thing it was asked to do. ### Why it fails locally Extracting the audio track is one command with one more tool, and that is precisely the cost: another binary to install, to keep current, and to have present on whatever machine the agent happens to be running on. It is a dependency you acquired for a step that produces nothing anyone wanted, because a WAV file is not an answer. Video containers are not one format either. MP4, MOV, MKV, WebM, each carrying whatever audio codec the recorder chose, sometimes more than one track, sometimes with the speech on a channel your default extraction ignores. The step handles the files you tested it on and fails on the one your user sends. And when it works you are exactly where you started: holding audio you still cannot transcribe. **The extra tool did not solve the problem, it added a stage to it.** The hard part, turning an hour of speech into attributed text, is still entirely ahead of you. ### The code `listen` accepts video and works on its audio track. Nothing is extracted on your side. Anything longer than a few minutes is queued and answers with a handle instead. ``` upload(filename="demo.mp4", content_type="video/mp4") -> key, upload_url curl -T demo.mp4 "" listen(key="", language="en") -> handle, engine, used_fallback, cost_eur, structure result(handle="", outline=true) -> who spoke, and when result(handle="", find="pricing") -> excerpts, each with its timestamp ``` ### What it costs `listen` costs 0.005 EUR per minute of audio in `balanced` mode, so a forty-five minute recorded call is about 0.23 EUR. `exact` costs 0.008 EUR per minute and uses the strongest model: worth it on a room microphone, on crosstalk and on heavy accents. Both separate speakers. **You are charged for minutes, not for megabytes.** A video weighs on the order of a hundred times its own audio track, and none of that weight reaches the bill. The duration is measured from the file itself rather than guessed from its size, and a declared duration that disagrees with the file's weight is not trusted. You pay once, on the real duration, whether or not you ever collect the result. The same file sent twice is recognised by its contents and not by its name, so a second attempt is one charge and not two. Querying the transcript afterwards with `result` is free, however many times you do it. ### What this does not do - The picture is not looked at. Slides on a shared screen, a whiteboard, anything written rather than spoken does not appear in the transcript. For a document, use `read`. - The audio track is what gets transcribed. A screen recording with nobody talking comes back with nothing in it, and that is the correct answer. - Anything longer than a few minutes is queued: `listen` answers immediately with a handle and `status: running`, and `result` serves the transcript once it is ready. - Four hours is the ceiling on a single file. Longer than that, split it. --- ## The recording is longer than any tool call is allowed to run > A long transcription cannot answer inside a tool call. It gets queued, you get a handle immediately, and the retry that follows a timeout does not double the bill. Source: https://mcpbelt.com/problems/transcribe-a-recording-longer-than-a-request ### The problem You call the transcription tool on an hour of audio and wait. The call has a deadline, the engine on the other side does not care about it, and the deadline arrives first. Your agent gets nothing back, which looks exactly like a service that is down. What happens next is the expensive part. **An agent that times out retries**, because retrying is the right thing to do when a call does not answer. The first attempt was never cancelled, so the same hour is now being transcribed twice, and unless somebody planned for it, paid for twice. The second attempt has the same deadline as the first, so it times out too, and so does the third. The failure is silent in the way that costs money: no error mentions duplication, no log says the work is still running somewhere, and the only visible symptom is a tool that never seems to work on long files. ### Why it fails locally This is not a speed problem, and a faster model does not fix it. A tool call is a request: it has to answer, and it has to answer before the client on the other end gives up. Work measured in minutes does not fit that shape however fast the minutes are, and running a speech model on your own machine makes it worse, since real time or slower on a laptop means an hour of audio takes an hour while the call sits there waiting. The shape that does fit is a job: start it, take a receipt, come back for the result. Building that yourself means somewhere to keep the job, somewhere to keep the output, a worker that outlives the process which accepted the request, and a way to hold the reserved credit across the two. The piece that gets written last is the one that matters: recognising that the retry which just arrived is the same work already in flight. Without it every timeout is a second engine call, and every engine call is real money leaving. **The retry is not an edge case, it is the normal behaviour of every agent you will ever connect.** ### The code Above a few minutes `listen` does not wait. It answers immediately with a handle and `status: running`, and the same handle serves the transcript once it is ready. ``` upload(filename="workshop.m4a", content_type="audio/mp4") -> key, upload_url curl -T workshop.m4a "" listen(key="") -> handle, status: running, estimated_cost_eur, estimated_minutes, retry_after_seconds result(handle="") -> status: running, waiting_seconds, retry_after_seconds result(handle="") -> the transcript, once it is ready cancel(handle="") -> cancelled, refunded_eur, reason ``` ### What it costs Queueing is not a different price. `listen` costs 0.005 EUR per minute in `balanced` mode and 0.008 EUR in `exact`, whether the answer comes back inside the call or hours later, so a fifty-six minute recording is about 0.28 EUR. Measured on a real run: fifty-six minutes of recording, queued, transcribed and delivered in thirty-five seconds. **The same request is charged once.** It is recognised by the contents of the file rather than by its name, so the retry that follows a timeout returns the first job instead of starting a second one, and this is what makes a timing-out agent harmless rather than expensive. If the operation fails, nothing is charged at all. Duration is measured from the file, not estimated from its size, and you pay on the real duration whether or not you ever collect the result. Waiting is free: `result` costs nothing however many times you call it while the job runs, and so does `cancel`. ### What this does not do - `cancel` returns the credit only while the job is still waiting. Once an engine has started, `cancelled` comes back false with a `reason`, the job runs to the end, and you collect it with `result` as usual. - `retry_after_seconds` is a hint about when to look again, not a promise about when the work will be done. - Nothing is pushed to you. There is no callback and no webhook: you come back with the handle. - Four hours is the ceiling on a single recording. Longer than that, split the file. --- # How the service behaves ## Every capability your agent lacks is another API key to manage > Two capabilities already meant four providers, four bills and four keys. This is one connection, prepaid, with no key to store anywhere. Source: https://mcpbelt.com/problems/stop-managing-one-api-key-per-provider ### The problem Your agent cannot read a scan, so you go and find something that can. Comparing the options takes an afternoon. Then there is an account, a card, terms someone should probably read, a key, a place to keep the key, and a response format that looks nothing like the last one you integrated. Then it needs to transcribe audio, and all of it happens again from the top. And each capability wants a second provider for the day the first one is down, so it was never one provider per capability, it was two. **Two capabilities, four providers**, four bills, four rotation schedules and four response shapes to normalise into whatever your code expected to receive. Not one of those four integrations is hard. The number is what hurts, and the number only goes in one direction. ### Why it fails locally There is no missing technique here, which is exactly why it never gets solved. Every piece is easy and every piece is permanent: the key that has to be present in development, in CI and in production, the rotation nobody schedules until an incident schedules it, the invoice from a provider you called twice last quarter, the normalisation layer that quietly drops a field the week a provider changes its response. A key in a configuration file is also the thing that ends up in a commit. Usually not through carelessness: a `.env` that was never in `.gitignore`, a fixture copied with real values still in it, a debug line printed into a log that goes somewhere else. Every provider you add is one more chance for that, and the chance is not additive in a comforting way. The fallback is the part that stays unbuilt. Writing a second integration against a provider you hope never to call is work with no visible reward, so the second provider remains a comment in the code until the first one is down, which is the one moment you have no time to write it. ### The code One line. The client registers itself, a person approves it once in a browser, and the account exists. There is no key anywhere in this. ``` claude mcp add --transport http mcpbelt https://mcpbelt.com/mcp/ -> the client registers and authorises itself -> the account exists with trial credit, nobody had to be there account() -> credits_available_eur, top_up_url account(estimate_for="listen") -> estimate_eur per mode, unit: minute ``` ### What it costs The account starts with 0.10 EUR of trial credit and no card. No signup, no email, no key to paste, and nobody has to approve anything: the client registers and authorises itself. Reading costs 0.007 EUR per page and listening 0.005 EUR per minute in `balanced` mode, and the whole list is on [pricing](/pricing). It is **prepaid credit**: no subscription, no per-call invoice, no monthly minimum, nothing to cancel. Top up when you want to, from 5 EUR. The tools that do not do paid work cost nothing at all: `upload`, `result`, `cancel`, `account`. One balance instead of four bills, and the second provider you were never going to write is already there. Every capability has more than one engine behind it, and if the first fails the second answers at the **same price with the same guarantees**. Each response says which one ran, in `engine` and `used_fallback`. ### What this does not do - There is no API key to issue. Authorisation is OAuth 2.1 with dynamic client registration and PKCE, which is what MCP clients implement. A client that expects a key in a header will not connect. - The account belongs to the browser that approved it. Approving from another machine creates a different account, with its own balance. - The capabilities are two: reading documents and transcribing audio. This does not stand in front of every model API you use, it replaces the providers behind those two. - Files are fetched from the address you upload them to and passed to an engine that runs elsewhere. If a document cannot leave your machine, this is the wrong tool. --- ## The provider your agent depends on goes down > A backup engine is easy to write. A backup engine that returns the same thing as the first one is the actual work. Source: https://mcpbelt.com/problems/what-happens-when-a-provider-is-down ### The problem The engine your agent calls stops answering, or starts answering in four seconds instead of one, or ships a change and returns a field with a different name. Your agent has one provider and no plan B, so the work stops. That is the visible failure, and it is the good one: somebody notices within the hour. The bad failure is the fix. Somebody wires in a second provider and normalises whatever comes back so the code keeps running. The second engine does not return per-page text, or does not separate speakers, or names things differently. The call succeeds, the shape is right, the answer is thinner, and the bill is the same. Nobody sees that one. There is no error, no alert and no retry. The only symptom is that the answers got worse on the days the first provider was having a bad time, and by the time anyone correlates it the logs are gone. ### Why it fails locally Writing a fallback is an afternoon. Writing a fallback that **guarantees the same thing** is the real job, and it is not a code problem, it is a contract problem: you have to decide what a call promises before you can tell whether the backup kept the promise. Without that decision, every provider difference becomes a silent downgrade. One engine gives you text page by page, the other gives you one block, so page ranges work on Tuesday and not on Wednesday. One separates speakers, the other returns nothing when it is not asked to, so asking for a time range works with the primary and returns empty with the backup. Two engines label speakers two different ways, and code that matched on the label breaks on nothing. The other half is knowing when to give up on a provider and when to keep going. An unreadable file will be unreadable on the second engine too, and retrying it just burns another call. A provider timing out is exactly the case you should retry. Sorting one from the other means classifying every error code from every provider, and the list is never finished. ### The code Nothing changes in the call. What changes is the response: `engine` names what actually ran and `used_fallback` says whether it was the backup. The price is the same either way. ``` read(key="", mode="exact") -> handle, engine, used_fallback, cost_eur, structure # when the first engine answered engine: "", used_fallback: false # when it did not: same mode, same cost_eur engine: "", used_fallback: true result(handle="", pages="12-14") -> the same call works either way: the mode guarantees per-page text, so an engine that cannot produce it never runs this job ``` ### What it costs The fallback is not a tier and not a surcharge. `read` in `exact` mode costs 0.016 EUR per page and `listen` in `exact` mode costs 0.008 EUR per minute whoever ends up doing the work. Which engine answered is our problem, and the full list is on [pricing](/pricing). A failed attempt is not billed. If the first engine fails and the backup succeeds, you pay once, at the mode price. If every engine in the chain fails, you pay nothing: the credit that was held for the job is released. Checking what a job will cost is free, like every call that is not work: `upload`, `result`, `cancel`, `account`. Call `account(estimate_for="read")` for the price per unit by mode before you commit to a large file. ### What this does not do - `used_fallback` tells you the backup answered. It does not mean the result is worse: the contract was checked before you got it, and the check is on the fields, not on the prose. - The contract is a floor, not a ceiling. Capabilities only some engines have stay outside it and show up in the response labelled by `structure_source`, so you can tell a section list the engine reported from one inferred from the text. - No uptime or availability figure is promised here, because none has been measured over a meaningful period. What is guaranteed is the shape of what you get back and the price you pay for it. - If a mode has no engine that can serve it, the call fails immediately and costs nothing. It does not quietly fall back to a mode that promises less. --- ## A long result does not cost you once, it costs you forever > Text that comes back inline stays in the conversation and is re-sent on every turn after it, including the turns about something else. Source: https://mcpbelt.com/problems/keep-long-results-out-of-your-context ### The problem A tool returns forty thousand characters and your agent reads them. The call took two seconds and it looked free. It was not: those characters are now part of the conversation, they go back up on every following turn, and they will still be there when the topic has moved on. That is the difference between a cost and a tax. The answer about the document is fine. The answer three turns later, about something unrelated, is worse, because the model is reasoning with a contract it no longer needs sitting between it and the question. An eighty-page document returned inline is roughly twenty thousand tokens of permanent furniture. There is no error for this and no line in a bill. The failure mode is a conversation that gets vaguer as it goes on, which everybody blames on the model. ### Why it fails locally Any tool that returns text returns **all** of the text, and it is not being stupid: it has no idea which part you wanted. Neither do you yet, which is the whole reason you called it. So the tool hands over everything and the decision about what matters happens after the context has already been spent. The workarounds each break something. Truncating answers questions about the beginning and lies about the rest. Chunking and summarising costs a model call per chunk and leaves you a lossy copy you cannot search: the clause you needed was in the sentence that got compressed. Writing the text to a file and reading it back moves the problem into a second tool that has the same flaw. The detail almost nobody handles is that a tool response has **two channels**. The text block is what the model reads; the structured fields are what the client code reads. They arrive together, in the same response, and anything present in both is paid for twice. A response that helpfully repeats the section titles in the structured output and again in the prose has just doubled the part that was supposed to be the cheap summary. ### The code Above an explicit, configurable threshold the response is a preview, a map and a handle. `text` is simply absent, `truncated` says so, and `tokens_if_inlined` tells you what you did not spend. Every `result` call after that is free. ``` read(key="") -> handle, engine, cost_eur, structure, chars: 152340, tokens_if_inlined: 38085, truncated: true # no "text" field at all result(handle="", outline=true) -> outline, structure_source # titles and pages, no body result(handle="", find="termination") -> matches, match_count, matches_omitted result(handle="", pages="12-14") -> text, chars_total, next_offset ``` ### What it costs The saving is measured, not claimed: on a forty-page document the response came back at **459 tokens instead of 6,355**. That is the same work, the same `read` at 0.007 EUR per page, with fourteen times less of your context spent on it. Querying the result costs nothing, however many times you do it: `upload`, `result`, `cancel`, `account` are free calls. Outline first, search second, then pull the window you need. The work was paid for when it ran, and paging through it is not more work. The threshold is a setting, not a hunch, and the split is deliberate on both channels: section titles and excerpts go in the text block because the model reads them, counts and offsets go in the structured fields because your code reads them, and neither is repeated in the other. ### What this does not do - `result` will not hand you the entire long result back one window at a time as a matter of routine. If you genuinely need all of it in context, nothing here saves you. - The full result is kept for a limited time. Once it expires the handle stops answering and the file has to be processed again, at the normal price. - `pages` works on documents and `time` works on transcripts. Asking for the wrong one is an error, not an empty answer. - `tokens_if_inlined` is an estimate of what inlining would have cost your context. It is a measure of the result, not a billing figure: you are charged per page or per minute, never per token. --- ## Your agent retried, and you paid twice > The same file processed again is the same money again. Replay is keyed on the content, so a repeat is recognised as a repeat. Source: https://mcpbelt.com/problems/do-not-pay-twice-for-the-same-file ### The problem An agent that times out retries, because that is what a well-behaved agent does. A workflow that runs on a schedule sends yesterday's file again. A person uploads the same contract under a slightly different name because they could not remember whether they had already done it. None of these is a bug. On a metered API every one of them is a second charge for a result you already have. The first call may even have succeeded: the answer got lost on the way back, the connection dropped, the client gave up one second before the response landed. The work was done and paid for, and it is about to be done and paid for again. The version that stings is the concurrent one. The retry arrives while the first call is still running, so there is nothing finished to hand back yet and nothing that looks like a duplicate. Two engines are now processing the same file, and both invoices are real. ### Why it fails locally The instinct is a cache in front of the call, and it solves the easy case while getting the one that matters wrong. A cache keyed on a filename misses the same document saved twice under two names, and hits on two different documents that happen to share one. Keyed on the request parameters, it misses the retry that resent the file rather than the reference. The deeper problem is that a cache and a ledger are **two separate records of the same money**. They will agree for months and then they will not: an eviction, a partial write, a deploy that clears one and not the other, and now the answer to whether you were charged depends on which system you ask. Reconciling those after the fact is an evening you do not get back. And a cache does not help with the concurrent retry at all, because at the moment the second call arrives there is no entry to hit. Stopping that one needs a lock taken before the work starts, not a lookup performed after it finishes. ### The code The same file read twice. The second call returns the first result: `cached` is true and `cost_eur` is zero. Nothing had to be configured for this and there is no cache to enable. ``` read(key="", mode="balanced") -> handle, engine, cost_eur, cached: false # the same bytes again, under any key or filename read(key="", mode="balanced") -> handle, engine, cost_eur: 0, cached: true # a retry that arrives while the first call is still # running is refused before an engine is paid, not # after. Ask the handle instead of starting again: result(handle="") -> status: running, retry_after_seconds ``` ### What it costs A replay costs nothing. `cost_eur` is zero and `cached` is true, and that is the whole of it: no reduced rate, no discount tier, no charge for the lookup. A first `read` in `balanced` mode is 0.007 EUR per page, and reading the same file again is free. Two cases that look like one. If the stored result is still there, it is handed back and nothing is charged. If it has expired, the file is processed again and you are charged again, because an engine really was paid for the second time. `cached` tells you which of the two happened, on every call. The identity is computed on the content, so the same bytes uploaded under a new name are the same job. It is not a cache sitting next to the billing: it is the same key the billing uses, which is why the two cannot disagree. You start with 0.10 EUR of trial credit and top up from 5 EUR: see [pricing](/pricing). ### What this does not do - Replay is per account. Another account reading the same document is a new job at the normal price. - The same file in a different mode is a different job, because it is different work: `balanced` and `exact` do not call the same engines. - Stored results expire. After that the handle stops answering and the next read is charged in full. - If the process dies between the engine answering and the charge being written, the retry calls the engine again. That window is narrow and it is real, and the number of retries is capped rather than left to run. --- ## An autonomous agent with a credit card has no ceiling > Usage billing means the agent decides how much to spend and you find out afterwards. Prepaid credit makes the ceiling a balance instead of a promise. Source: https://mcpbelt.com/problems/cap-what-an-agent-can-spend ### The problem You gave the agent a key so it could get on with the work without asking you every time. That key bills by usage, which means the agent now decides how much money leaves the account, and nothing in the arrangement says how much is too much. A loop that goes wrong and retries three hundred times is not a rare event, it is Tuesday. The retry is usually the correct behaviour: a call timed out, so the agent tries again, and it has no way to know that the first one succeeded and was paid for. Multiply that by a file that gets re-read on every turn because nobody kept the handle, and the bill is not a bug in one place, it is arithmetic. The failure is not that money was spent. It is that the spending had no upper bound at the moment the agent made the decision, and by the time anyone could see the number, it had already happened. ### Why it fails locally Provider spend limits do not stop anything. They are computed from usage that is aggregated after the fact, they fire hours later, and what they produce is an email. **An alarm is not a brake.** By the time the threshold is evaluated, the three hundred calls are done and billed. Wrapping the calls yourself and counting them is better and still not enough, because a counter can only tell you what you already spent. To stop a call before it happens you need the price of that specific call before making it, and the price depends on the size of the input, which the wrapper does not know until it has read the file. And the whole scheme rests on the agent not holding the key directly. The moment it does, every guard you wrote is a suggestion: the agent can call the provider without going through your wrapper, because that is exactly what having a key means. ### The code `account` is free and is never charged. Ask it what a job will cost before running it, and read the balance that is the actual ceiling. ``` account() -> credits_available_eur, top_up_url account(estimate_for="read") -> estimate_eur per mode, unit: page read(key="", mode="balanced") -> handle, engine, cost_eur # when the balance does not cover the job, nothing runs: # error credits_exhausted, carrying what it costs, what is # left, and a top-up link to hand to whoever pays. # Top up, call the same tool with the same arguments, # and you are not charged twice. ``` ### What it costs Credit is prepaid. There is no subscription and no per-call invoice: the account holds a balance, the balance is the ceiling, and an agent cannot spend past it because there is nothing there to spend. You start with 0.10 EUR of trial credit and no card, and top-ups start at 5 EUR. Before committing to a job, the amount it could cost is set aside, and **the charge never exceeds what was set aside**. A bill above the estimate is not something you have to watch for, because it cannot be produced. If the work fails, nothing is charged at all, and the same request sent twice is charged once. Checking any of this is free: upload, result, cancel, account are never billed, at any volume. `read` is 0.007 EUR per page in `balanced` mode and `listen` is 0.005 EUR per minute, and `account` will tell the agent both, per mode, before it decides. Out of credit, the tool refuses and says so in a message written to be repeated to the person paying. It does not quietly buy a cheaper, worse result to stay inside the budget. ### What this does not do - The ceiling is the balance, and there is nothing finer. No per-tool budget, no daily cap, no spend alerts: you control the exposure by how much credit you put on the account. - Credit is not refunded on request. It comes back only when a queued job is cancelled before an engine has started on it. - A job already running cannot be capped mid-flight. It was priced and reserved before it started, and that reservation is the limit. - Prices are per page and per minute of the real file, counted from the file itself rather than guessed from its size. An estimate given before the upload is an estimate. --- ## You queued the wrong file, and the money has not left yet > While a job is still waiting its turn, cancelling it gives the credit back. Once an engine has started, it cannot, and the tool says so. Source: https://mcpbelt.com/problems/cancel-a-job-that-has-not-started ### The problem The agent queued an hour of audio and it was the wrong hour of audio. Or it was the right one, and the person paying changed their mind thirty seconds later. Either way the job is sitting in a queue, nothing has happened to it yet, and money is about to be spent on a result nobody wants. This is a normal amount of wrong. An agent working from an instruction picks a file from a list, and lists have neighbours. The mistake is cheap to make and, in most setups, impossible to take back. What you want in that moment is narrow and specific: stop the job that has not started, get the credit back, and be told plainly if you were too late. Not a best effort, not a maybe. ### Why it fails locally With a provider API there is usually nothing to cancel, because there was never a queue. You made a call, the call is running, and the charge was incurred the moment the request was accepted. There is no gap to cancel inside. What is often called cancelling is closing the connection, and that only stops you listening. The work continues on the other side and is billed in full: you have thrown away the result you paid for, which is worse than not cancelling. On the long-job APIs that do have a cancel endpoint, the honest reading is the same, because the meter starts when processing starts and cancelling after that recovers nothing. Building the queue yourself moves the problem rather than solving it. Now you hold jobs in your own queue and you have to decide, at the exact instant a request to cancel arrives, whether a worker has already picked the job up. Get that race wrong in one direction and you refund a job that ran; get it wrong in the other and you charge for one that never did. ### The code `cancel` is free and takes the handle a queued job returned, the same one you would pass to `result`. ``` listen(key="") -> handle, status: running, estimated_cost_eur, estimated_minutes cancel(handle="") -> cancelled: true, refunded_eur # once an engine has started, the same call answers honestly: # -> cancelled: false, refunded_eur: 0, reason # and the job runs to the end, so you still collect it: result(handle="") -> the transcript, as usual ``` ### What it costs `cancel` costs nothing. It is one of the free calls, along with upload, result, account, and calling it on a job you turn out to be too late for is free as well. A cancelled job returns the credit that was set aside for it. `refunded_eur` is the amount that came back, and it is a real release of reserved credit, not a coupon and not a gesture. That reservation is also why the boundary is where it is. Credit is committed before the work starts and settled when it finishes, and the charge never exceeds what was committed. While the job waits, the commitment can be undone. After an engine has read the file, the money is out, and no answer we could give would put it back. ### What this does not do - It works only while the job is still waiting. `cancelled` comes back false when it is too late, and `reason` says why: that is the whole contract, and it is not softened anywhere. - Work that ran inline never had a queue to sit in. There is nothing to cancel, only a result you already have. - Cancelling is not deleting. It stops a job that has not started; it does not remove a finished result, and results expire on their own. - Calling it twice on the same handle is safe and changes nothing. The second call is not an error and does not refund twice. --- ## Connecting a new tool usually costs more than trying it > One command, and nobody has to be there. No signup, no email, no page to approve, no API key to paste into a config file and remember to rotate. Source: https://mcpbelt.com/problems/connect-an-mcp-client-in-one-line ### The problem Trying a new tool normally means creating an account, verifying an email, landing in a dashboard, generating a key, pasting it into a config file, restarting the client, and putting a note somewhere about rotating that key later. Seven steps, and none of them tell you whether the tool is any good. You pay all of that before you learn anything. The evaluation you actually wanted was two minutes long: hand it a file, see if the answer is right. Instead the cheapest part of the process is the part that comes last. For an agent it is worse than tedious. Every one of those steps is a human step, in a browser, with an inbox involved. The thing that was supposed to work on its own cannot get past the front door on its own. An agent with no browser at all, running in CI or on a server, does not get through any of it. ### Why it fails locally The friction is not accidental, it is the shape of API-key auth. A long-lived secret in a config file has to be created by someone, stored by someone, and rotated by someone, and each of those someones is a person doing manual work. That is the price of the mechanism, not of the product. It also does not fit the protocol. MCP authorisation is OAuth 2.1, and that is the only scheme in the specification: dynamic client registration, PKCE, tokens the client obtains and refreshes by itself. **A key in a header is not an option the clients implement**, so a service that only offers one is a service most clients cannot talk to at all. Which leaves the awkward middle: an agent that can call the tool but cannot get the credential to call it with, waiting for a person to finish a signup flow it is not allowed to touch. ### The code One command. The client registers itself, authorises itself, and the account exists with trial credit on it. Nobody has to be at the keyboard. ``` claude mcp add --transport http mcpbelt https://mcpbelt.com/mcp/ # the client registers and authorises itself, and that is the whole signup # with no browser, five plain HTTP calls do the same thing: see /llms.txt account() -> credits_available_eur, top_up_url upload(filename="invoice.pdf", content_type="application/pdf") -> key, upload_url curl -T invoice.pdf "" read(key="") -> handle, engine, cost_eur, structure, text ``` ### What it costs Connecting costs nothing and there is no card involved. The account is created with 0.10 EUR of trial credit on it, which is enough to read a few documents or transcribe a few voice notes and decide whether any of this is worth paying for. After that, credit is prepaid: no subscription, no per-call invoice, top-ups from 5 EUR. Only work is billed. upload, result, cancel, account are free at any volume, so an agent can check the balance, price a job, page through a result and cancel a queued one without spending anything. Then the work itself: `read` at 0.007 EUR per page and `listen` at 0.005 EUR per minute in `balanced` mode. Six tools arrive with the connection and there are no others to discover later. ### What this does not do - A person is asked for exactly two things, and neither is here: authorising a client against an account that already holds paid credit, and paying. Everything before that happens without anyone. - Because a client identifier is issued fresh on every registration, an agent that registers from scratch every session lands on a new account each time, with a new trial. That is why the token response carries a refresh token that never expires: keep it, reuse it, and you stay on the same account with the same balance. - Streamable HTTP, protocol revision 2026-07-28, OAuth 2.1 with dynamic client registration and PKCE. The previous revision is served from the same endpoint, so older clients keep working. - Refresh tokens rotate on every use, as the specification requires for public clients. A client that reuses an old one is treated as compromised and the whole chain is revoked. - The tools declare what they are: which ones are read-only and free, which ones spend credit and reach outside, which one is destructive. That is what a client reads when it decides what it may approve on its own.