You have a list of products from your database and want the model to pick the ones that match a user's query. The obvious move is to paste the rows as they are, with their real IDs, and ask the model to return the IDs of the matches. It works on the first try, and that's why nobody looks at it again. But database IDs are made for machines that compare bytes, not for models that read tokens, and in a prompt like this they can end up being most of what you pay for.
What you're paying for
Model APIs bill per token, a chunk of text that averages about three-quarters of an English word. Input tokens are what you send, and output tokens are what the model writes back. Output usually costs five or six times as much as input.
A UUID is a random 128-bit identifier written as 36 characters of hex and dashes, like 89fa6a68-8fb5-427b-beb7-99193f22faf8. Lots of databases use them as primary keys. A tokenizer, the part that splits text into tokens, learned its vocabulary from ordinary text. Random hex isn't ordinary text, so it gets chopped into short pieces of one to three characters each.
Measuring it
I built a typical "pick from a list" prompt: 46 tokens of instructions, 50 products as id | name lines (names like "wireless mouse" or "bamboo cable organizer"), a one-line query, and an answer that's a JSON array of three IDs. Then I swapped in different ID formats and counted tokens with tiktoken, OpenAI's open-source tokenizer library. Other vendors' tokenizers count a little differently, but random strings are expensive for all of them.
One UUID came out at 23 tokens on average. The product name next to it was about 4. So with UUIDs, 80% of the input is the IDs.
PRICE_IN, PRICE_OUT = 2.00, 12.00 # GPT-5.6 list price, $ per 1M tokens MONTH = 300_000 # 10,000 queries a day for 30 days for name, tin, tout in [("UUID", 1439, 72), ("list index", 336, 9)]: # measured cost = (tin * PRICE_IN + tout * PRICE_OUT) / 1e6 print(f"{name}: ${cost:.6f}/call ${cost * MONTH:,.2f}/month") Prices are list prices, meaning the ones each vendor publishes, taken from the catalog of the LLM gateway I work on on 9 November 2026. On GPT-5.6:
| ID format | Tokens per ID | Input / output | Per month | vs index |
|---|---|---|---|---|
| UUID v4 | 23.1 | 1,439 / 72 | $1,123 | 4.80x |
| ObjectId (24 hex chars) | 14.1 | 991 / 45 | $757 | 3.23x |
prod_ + 14 letters/digits | 11.0 | 835 / 37 | $634 | 2.71x |
| Database integer, 7 digits | 3.0 | 436 / 15 | $316 | 1.35x |
| List index 1..50 | 1.0 | 336 / 9 | $234 | 1.00x |
Switching from UUIDs to list numbers saves $889 a month at this volume. The prompt says exactly the same thing either way. The multiplier barely depends on the vendor: GPT-5.6-mini gives the same 4.80x (just $112 vs $23 a month), and Claude Haiku 5.5, Claude Sonnet 5.5 and Gemini 3.8 Flash land at 4.72x. What sets it is how much of your prompt is IDs compared to actual content, not whose model you call.
The output side is worse than it looks
The answer has only three IDs, but it's 72 tokens with UUIDs and 9 with indexes, and those tokens are billed at output price. On GPT-5.6, the output is 23% of the UUID call's cost.
There's also a reliability problem the bill doesn't show. When the model copies a 36-character random string, it has to reproduce every character exactly, and it can't check its work against anything. If one character comes out wrong, you get an ID that matches no row in your database, or worse, a confident answer your code throws away. I didn't measure how often that happens. You don't need a rate, though, to see that copying "22" is easier to get right than copying 36 random characters.
Give the model numbers, keep the IDs in your code
The fix is about ten lines. Number the items in the prompt, keep the mapping on your side, and translate the answer back, rejecting anything that isn't on the list.
import json def build_listing(rows): """rows: list of (real_id, name). Returns prompt text and index -> real_id map.""" lines = [f"{i} | {name}" for i, (_, name) in enumerate(rows, start=1)] return "\n".join(lines), {str(i): rid for i, (rid, _) in enumerate(rows, start=1)} def parse_answer(answer_text, index_map): picked = [str(x) for x in json.loads(answer_text)] return [index_map[p] for p in picked if p in index_map] # drop unknown numbers listing, index_map = build_listing([("89fa6a68-8fb5-427b-beb7-99193f22faf8", "cable box"), ("62801c45-1043-4a10-98ae-43346c12ace8", "desk lamp")]) print(listing) print(parse_answer('["1", "7"]', index_map)) # "7" isn't on the list, so it's dropped The map lives only for one request, so you don't need to store anything. If you can't change the prompt format right now, the database integer is the next best thing at 1.35x, and anything random-looking (UUIDs, hashes, ObjectIds, prod_-style keys) is the expensive end.
What this doesn't solve
Indexes only work inside one call. If the model has to refer to an item across turns of a conversation, across tool calls in an agent, or in output you store and read later, it needs a stable ID. In that case, a short integer key or a slug like cable-box is still much cheaper than a UUID.
I measured cost, not accuracy. I don't have a number for how often a model mistypes a UUID, and a short index can be wrong too: the model can pick the wrong number just as it can pick the wrong UUID. The validation step catches made-up numbers, not wrong choices. My prompt is also ID-heavy by design. If each item comes with a paragraph of description, the IDs' share drops and so does the savings. Run the counts on your own prompt before you rewrite anything. And no gateway can do this for you, including the one I work on: it sees the IDs in your text, but it can't know which ones your code needs back.
The takeaway
Look at your prompt the way the tokenizer does. Every random-looking ID costs 11 to 23 tokens going in, and again at output price coming out. Give the model numbers, keep the real IDs in your code, and treat anything not on the list as an error.

