One page of 1-bedroom rentals in Dubai Marina from the Bayut API is 68,709 characters of JSON. At the usual rule of thumb of four characters per token, that is about 17,000 tokens of an AI agent's context window for 25 listings, and an agent that compares three areas on two portals has spent about 105,600 tokens before it writes a word of its answer.
I wanted to know where those tokens go, because "give the agent an MCP server" is now the default answer to "how do I connect an LLM to property data". So I connected three UAE real estate APIs the way an MCP setup does, called them for a simple question, and measured everything the model would have to read.
What I measured
The question was the one people actually ask an agent: what does a 1-bedroom flat rent for in Dubai Marina, Jumeirah Village Circle and Business Bay?
On 3 November 2026 I made 18 calls through three APIs: the Bayut API, the Property Finder API and the UAE Real Estate API, which serves both portals behind one key. For each area I looked the location up on both portals, ran one page of 1-bedroom rental search on each, and pulled Property Finder's lease transactions. I also fetched one full listing record and ran the UAE Real Estate search for both portals in Dubai Marina.
Every endpoint of these APIs is reachable over MCP through RapidAPI's gateway, one tool per endpoint (setup here). A tool's result is the API's response, so I measured the responses themselves: characters of compact JSON, divided by four for a token estimate. Real counts depend on the model's tokenizer (Anthropic's token counting docs show how to get exact ones), but the comparisons below hold either way, because every number gets the same treatment.
Cost one: the tool list, before the first question
An MCP client sends the model the name, description and input schema of every tool it can call, on every turn. I built those definitions from the three public OpenAPI files the way a gateway does, one tool per endpoint, with each query parameter as a property of the tool's input schema.
| API | Tools | Characters | About tokens |
|---|---|---|---|
| Bayut | 17 | 17,828 | 4,457 |
| Property Finder | 18 | 23,886 | 5,972 |
| UAE Real Estate | 11 | 10,725 | 2,681 |
| All three | 46 | 52,439 | 13,110 |
Property Finder costs the most: it has more endpoints, and its tools average about 1,327 characters against Bayut's 1,049, with parameter descriptions making up 48.3% of the text. Those descriptions list allowed values, defaults and examples for every filter. That detail is exactly what helps a model call the tool correctly, so I would not cut it. It is the reason to load only the APIs a task needs.
The other approach is a discovery connector. Free Live Data MCP, an open-source connector we maintain, puts 48 APIs behind four tools (search_apis, search_endpoints, describe_endpoint, call_endpoint). Its four definitions are 2,146 characters, about 536 tokens. The model pays for a parameter list only when it asks for one. The trade is an extra round trip or two per task.
Cost two: the responses
For Dubai Marina, one page of Bayut rental search cost about 17,177 tokens and one page of Property Finder search about 16,324. The full Property Finder listing record was about 3,216, the lease transactions about 1,794, and Property Finder's location lookup only 470. Bayut's lookup cost about 2,023, because it returned 25 candidate locations with their full paths and names, where Property Finder returned 20 short ones.
Across the three areas the search pages were remarkably stable: Bayut between 17,098 and 19,527 tokens, Property Finder between 16,324 and 18,126. The size is a property of the format, not of the area.
Where the characters go
A Bayut search row ran to between about 2,700 and 3,100 characters, depending on the area. The biggest field is location at 28.3% of the row. It is the whole location tree, four to six levels from UAE down to the building, and every level carries its name in several languages: name, then name_l1 in Arabic, name_l2 in Chinese and name_l3 in Russian. category takes another 16.8% for the same reason: two levels, each in several languages. The agent needs one word from each.
On Property Finder, images is 37.8% of a row: image URLs with long version strings. location_tree is another 9.0% and address 7.2%.
The part I did not expect was the contact data. On Bayut, the listing agent (ownerAgent), the phoneNumber and contactName fields together are 14.1% of every row. On Property Finder, agent_details is 14.7%. An agent that forwards raw rows is paying to read people's names and phone numbers it has no use for, and putting them into a model's context and logs. Trimming fixes both problems at once.
The fix: trim before the model reads it
The agent needs about nine fields to answer a rent question. Here is the Bayut version, runnable as printed with a RapidAPI key:
import json import os import requests HOST = "uae-real-estate3.p.rapidapi.com" # Bayut API on RapidAPI BASE = os.environ.get("API_BASE", "https://" + HOST) def headers(): if "API_BASE" in os.environ: return json.loads(os.environ.get("API_HEADERS", "{}")) return {"x-rapidapi-host": HOST, "x-rapidapi-key": os.environ["RAPIDAPI_KEY"]} def cost(x): """Characters of compact JSON, and a rough token count at 4 characters a token.""" n = len(json.dumps(x, ensure_ascii=False, separators=(",", ":"))) return n, round(n / 4) def trim(row): """The nine fields an agent needs for a rent question, flat, with the unit in the key.""" return { "id": row["externalID"], "title": (row.get("title") or {}).get("en"), "rent_aed": row.get("price"), "per": row.get("rentFrequency"), "beds": row.get("rooms"), "baths": row.get("baths"), "area_sqm": round(row["area"], 1) if row.get("area") else None, "furnished": row.get("furnishingStatus"), "building": (row.get("location") or [{}])[-1].get("name"), } # One page of 1-bedroom rentals in Dubai Marina (Bayut location id 5003) body = requests.get( BASE + "/search-property", params={"purpose": "for-rent", "location_ids": "5003", "rooms": "1", "page": 1}, headers=headers(), timeout=60, ).json() rows = body["data"]["properties"] small = [trim(r) for r in rows] print("rows:", len(rows)) print("full response: %d characters, about %d tokens" % cost(body)) print("trimmed rows: %d characters, about %d tokens" % cost(small)) print(json.dumps(small[0], indent=2)) On my run it printed 25 rows, 68,709 characters (about 17,177 tokens) for the full response and 4,907 characters (about 1,227 tokens) after trimming. The first row came out as a furnished 1-bedroom in Marina Diamond 4 at AED 70,000 a year, 62.6 square metres: everything the agent needs to answer, nothing it does not.
Across the three areas the median Bayut page went from about 17,177 tokens to 1,227, a cut of 92.9%. Property Finder went from about 17,391 to 2,460, a cut of 85.5%. Its trimmed rows keep the listing URL, which an agent needs to show a link, and that URL is long. The six search pages together went from about 105,600 tokens to about 11,250.
Where the trim runs matters less than that it runs. In your own agent it is a few lines around the tool call. With a gateway you do not control, put a thin MCP server of your own in front of it that calls the API and returns the trimmed rows. A generic gateway has no way to know which fields matter to your task.
Pitfalls I hit in the data
Pretty-printed JSON costs half as much again. The same 18 responses were 52.4% longer indented with two spaces than in compact form. Indentation is free for a human reading a log and expensive for a model reading a tool result. Serialize with separators=(",", ":").
Bayut's area filter and area field use different units. The area_min and area_max filters take square feet. The area field on the 1-bedroom rows came back as values like 62.6, with fourteen decimals, which only makes sense as square metres. Property Finder's size says its unit ({"value": 592, "unit": "sqft"}). Put the unit in the key name (area_sqm, area_sqft) so the model cannot compare the two portals in different units without noticing.
The two portals disagree on types. Bayut's rooms is a number, Property Finder's bedrooms a string ("1"). Bayut's createdAt is a Unix timestamp in seconds, Property Finder's listed_date an ISO string. A model will usually cope, but normalizing once in the trim is cheaper than hoping.
Some Property Finder search rows have no rent at all. 14 of the 113 rows across my three rental searches carried no price.value. They are new-project cards mixed into the results, and 12 of them carry a price.from that is a sale price, such as {"from": 1000000, "down_payment_percentage": 20}. Drop them before the model sees the page, or it may read a million-dirham sale price as a rent.
Location ids belong to one portal. Dubai Marina is 5003 on Bayut and 50 on Property Finder. An agent that reuses an id across portals does not get an error; it gets the wrong area. Make the agent look the place up on each portal, or keep a small table of ids for the areas you care about.
Bayut search rows have no description. description came back as {"en": null} on all 75 search rows I pulled, so an agent that wants the listing text has to fetch the full record, which costs more tokens again. For a rent question it does not need it.
What I would do next
For a single-portal agent, one API's tools plus a trim on the search results is enough: about 4,500 to 6,000 tokens of tools and about 1,200 to 2,600 per trimmed search page. For an agent that has to choose among many sources, a discovery connector keeps the fixed cost near 500 tokens.
The APIs I used are the Bayut API (free plan on RapidAPI), the Property Finder API and the UAE Real Estate API, which is also a Postman collection if you want to try the calls before writing code. Each portal has its own site with agent setup notes: bayutapi.dev for Bayut, with a function-calling guide, and uaepropertyapi.com for Property Finder, with MCP setup for Claude, Cursor and ChatGPT. For what the Bayut data itself covers, see the Bayut API overview on LinkedIn.
Building an agent on UAE property data and want the trimmed version as a service, or a dataset instead of calls? Get in touch.



