A Meta Marketing API you can point a test at
This host answers Graph requests the way graph.facebook.com answers
them, over a publicly-valid certificate, with no Meta login, no app review and no
request leaving this machine. It exists so the browser Meta route
— MCP → console → engine → extension → Graph — can be run end
to end before it is ever pointed at a real advertiser's account.
Point at it
graph.the-best-meta-ads-bench-simulator.cc
One constant. In the extension that is GRAPH_HOST in
client/src/broker/graph.ts, and it must stay a build-time
value: a host that a server could name is an SSRF primitive aimed out of a browser
that is logged in to Meta, which is worse than the problem the browser route was
chosen to avoid. Everything else — the version, the node shape, the edge names,
the field spellings, the error bodies — is already what the client sends.
- API version
- v26.0
- Ad account
- act_10000000000001
- Access token
- EAAsim.default
- Second account
- act_10000000000002
It is unhelpful on purpose
A simulator that answered the way a client hopes would endorse that client's model of Meta rather than test it. So this one reproduces the parts of Graph that people get wrong:
| What it does | Why that matters |
|---|---|
| HTTP 400 for every error class | a dead token, a throttle, a missing object and a bad version are all 400.
The verdict is error.code in the body, never the status. |
paging.next carries access_token= |
Graph builds that URL from the request it received, so a token comes back inside the response body and into whatever reads it next. |
promote_pages rows carry a Page access_token |
a write-capable credential nobody asked for, in the default representation of a node you did ask for. |
| an unknown field is dropped in silence | misspell one and you get a row with a missing column, not an error. This is the whole reason a curated field list exists. |
| money is a string of minor units | "5000" is 50.00. Treat it as major units and you are wrong by
100×, in the direction that looks plausible. |
| ids are strings of ~15 digits | parse them as numbers and they corrupt silently past 253. |
status and effective_status disagree |
an ACTIVE ad set inside a paused campaign reports
CAMPAIGN_PAUSED and spends nothing. |
Make it fail
Every error class the client claims to handle is reachable by swapping the token. Nothing breaks; the workspace is untouched.
| Token | code | subcode | meaning |
|---|---|---|---|
EAAsim.expired | 190 | 460 | session expired — permanent, only the operator can fix it |
EAAsim.throttled | 17 | — | user request limit — retryable |
EAAsim.buc | 80004 | — | business use case limit — retryable |
EAAsim.forbidden | 200 | — | missing ads_management permission — permanent |
EAAsim.oops | 2 | — | Graph's own fault — the one response that is 500, not 400 |
State
One shared workspace that always works, so a bench run needs no setup. Any token
spelled EAAsim.<name> forks its own private copy on first use, so two
agents writing at once do not collide. POST /api/adsim/reset puts either
one back to the seed, and so does restarting the container.