The application is the URL

Here is a tool for working out whether you can afford to quit your job:

https://upshot.fyi/v2/#a=Whether+I+have+enough+runway+to+quit&h=Nine+months+is+the+number+to+beat&v=Anything+under+that+and+a+slow+job+market+decides+for+you%2E&m=GPT-5&d=2026-09-10&g=Runway&i=cash:18000:Cash+saved&i=burn:2200:Monthly+burn&i=target:9:Months+you+want&r=months:cash/burn:Runway&t=Ready+to+walk:months%3E=target:Go+now&t=Ready+to+walk::Not+yet&u=cash:%C2%A3&u=burn:%C2%A3&u=target:months&u=months:months

Click it and you get a working calculator. Change the numbers and the answer moves. Push your runway past the target and the verdict at the bottom changes its mind. The URL moves with all of it, so you can send it on and the next person gets your version, your numbers included.

There is nothing behind it. No server, no database, no account, no build step, no dependencies. The tool is the string.

Making one is a sentence

You are already in ChatGPT or Claude, working something out. You type:

dinner was 80 between the three of us. make me something I can put my own numbers into. read https://upshot.fyi and export this

ChatGPTupshot.fyi/v2/#a=Splitting+dinner+three+ways&h=Split+the+bill&v=Divides...

Nothing installed at any step

The model fetches the page, writes a URL and hands it back. You click it. That is the product. The rest of this is how it stays that simple.

There is nothing to install. No extension, no plugin, no agent skill, no MCP server, no API key, no account, no signup. It works in chat apps I have never tested and in models that did not exist when I wrote it, because the only thing being integrated is a page.

The contract lives at llms.txt, and the homepage carries the same text in full, as static copy rather than something drawn by script. That duplication earns its keep. A model that lands on the homepage instead of the spec gets the whole contract.

Upshot never calls a model. You do, in a window you already had open.

Nothing you type leaves the browser

Everything after the # stays on your machine. That is not a trick. It is just how URLs work. The browser asks the host for /v2/index.html and stops there. The rest of the link never goes near the wire.

THE LINK YOU PASTE upshot.fyi/v2/#i=bill:80:Bill&r=each:bill/n sent never sent Static host Hands back one file, 22 KB. Same file for everybody. Never sees a card. Your browser Reads the card out of the link. Does the arithmetic. Draws it.
The host serves one static file and has no idea what anyone is looking at.

Two things fall out of that.

It costs nothing to run. One static file on a CDN. A million people open a million different tools and it is still one file. Nothing to store, nothing to scale, no bill that climbs when something takes off. I have never paid a penny to run it. There is also nothing to leak, because nothing is kept.

I cannot touch a card once it is sent. No takedown, no stats, no hotfix. If someone pasted a card into a group chat in March, that chat is the only copy and I will never know it exists.

It stopped being a format and became a language

The first version drew a fixed layout. Then it needed a checkbox. Then a number that feeds another number. Then a line that changes what it says when you cross a limit. Somewhere in there it turned into a small UI language.

IN THE LINK ON THE CARD i=bill:80:Bill 80 Bill r=each:bill/n:Each pays Each pays £26.67 bill/n t=Split:each>25:Steep Split Steep 26.67>25 Change the 80 and every row below it follows.
Nine keys, one row each. Formulas can use any name on the card, so a card is a tiny spreadsheet that knows how to lay itself out.

The whole vocabulary is nine keys: a bullet, a numbered step, a fixed fact, a checkbox, a number box, a pick-one choice, a calculated result, a line whose wording changes, and a unit. That is it. There is no key for a colour, a size or a layout. The card decides how it looks. The language only says what is on it.

Formulas get names, arithmetic, comparisons and a dozen functions. A result can use another result. A checkbox counts as 1 or 0. A choice group carries whichever number you picked. That covers tax bands, loan repayments, unit conversion and most of what people ask for.

The version is in the path

/v2/ is the current renderer. /v1/ is frozen and still serves the cards written against it. Any change that would make an existing card parse, calculate or draw differently does not get made. It gets a /v3/ and a new file.

So every new key has to be worth freezing forever, in public, with no way to take it back. Almost nothing is. That rule has killed more features than good taste ever would.

The renderer

One HTML file. 67 KB, 22 KB over the wire. No dependencies, no build step, no framework.

It does four things in order. Read the card out of the link. Check it makes sense. Work out the formulas. Draw it. Then it sits there, and every time you change a box it redoes the last two and rewrites the URL.

Formulas go through a small parser written by hand. There is no eval anywhere near it, because the input is a string off the internet.

A formula can use a result defined further down the card, so the order you wrote them in does not matter. The renderer works out what depends on what and fills each one in as it becomes answerable. If two formulas need each other, neither ever becomes answerable, and both show a dash.

A name that is defined nowhere counts as missing rather than waiting. That sounds like a detail. It is the whole difference between a typo and a real loop, which otherwise look identical from the inside.

There are two ways to fail, and keeping them apart matters:

  • The card does not make sense, so nothing is drawn. Two things claiming the same name, a unit for a name that does not exist, a key the language does not have. There is no honest way to draw half a card that argues with itself.
  • One value cannot be worked out, so that value shows a dash. The rest of the card is fine and still draws.

What it never does is check whether a number is right. It is a rendering layer. If the model used last year's tax bands you get last year's tax bands, laid out beautifully and completely wrong. Getting the facts right belongs to whatever wrote the card, and writing that line into the contract stopped a lot of scope creep.

The links have to survive chat

Every card gets sent to somebody, so the real spec is not what is legal in a URL. It is what survives WhatsApp.

  • A matching pair of _ anywhere in the link is eaten as italic before anyone clicks it. So is *, so is ~. The formula j*45 arrives as j45 and quietly means something else.
  • Autolinkers cut the link at the first character that is not legal in a URL, and < and > do it. You get half a card. The half that survives still draws, which is why nobody notices.
  • A full stop at the end reads as punctuation and gets trimmed off.

So everything is percent encoded except letters, digits and a hyphen. Formulas also keep + - / =, because there they are arithmetic.

The one that stung: the renderer writes your edits into the URL too, and it was using ~ to separate them. It was generating links that chat apps broke. You only find that by sending yourself a few hundred messages.

The prompt is the product surface

All of that ease rests on one file of English. The contract does the job an API would normally do, with no runtime anywhere to catch a mistake.

I expected this to be the easy half. It was not.

Write the contract llms.txt, in plain English Generate a few hundred real cards with the models people actually use Open every one and read it the pass rate cannot see a wrong number
Round and round. The renderer never moves in this loop, only the English does.

Three things I would not have guessed:

Examples teach louder than rules. A model copies the worked example and skims the paragraph above it. For a day the spec said one thing about encoding and an example did another, and every generation followed the example. The tests now check each shipped example against the rule it is meant to show. A stale example is worse than no example.

You cannot reason about what a model will write. Every confident guess I made about phrasing was wrong, and generating a hundred cards and reading them settled it every time. Which wording a model takes literally, what it invents when the language has no word for what it wants, whether a rule is teachable at all: all cheap to measure, all pointless to argue about.

A passing test suite cannot see a wrong number. A card can parse cleanly, draw beautifully and be completely wrong. The tests prove the renderer drew what the URL said. They say nothing about whether the model should have said it. Treating those as one problem wasted a week.

What it costs

  • About 2000 characters. Real cards land between 600 and 1000. There is no compression and there will not be. The model has to type the URL and you have to be able to read it. Zipping it would fit more in and kill both.
  • No privacy beyond "it is a URL". Anyone holding the link can read the card, and it sits in browser history, chat logs and whatever it passed through on the way.
  • No free text, no charts, no images, no network. The list of keys is closed. Adding one per request is how this turns into a bad framework.
  • No way to fix a card after you send it.

Why it holds up

The appeal is not that it is small. Plenty of things are small and annoying. It is that the whole thing is one string you can read, paste, diff, keep and send, and nothing is needed to run it but a browser somebody already has.

For small tools that is enough far more often than I expected going in. When it stops being enough, what runs out is the character count, not the idea.

The renderer, the language and the tests are on GitHub. The contract is at llms.txt, and /made/ is what people have pointed their models at so far.