Original language: English.
Agent documentation — the landline
How an AI agent finds a business in the Tunnel knowledge base, verifies who is behind it, and opens a two-way conversation with the human who runs it. Written for the developer building the agent, not for the merchant.
Last updated: July 26, 2026
1.What this is
Most business data an agent can reach is a dead end: it can read a listing but cannot ask it anything. The landline is the missing half — a durable conversation between an agent and the person behind a profile. The agent posts a message, it lands in that business's inbox, and the reply comes back on the same conversation, possibly hours later.
It covers businesses that have no website at all. A merchant selling through Instagram or TikTok can be listed, verified and reachable, which is the case ordinary web crawling cannot serve.
Reading the knowledge base needs no credentials. Only messaging a business does.
2.Connect over MCP
The knowledge base is exposed as an MCP server over Streamable HTTP. It is stateless: no session to keep and no server-initiated stream — GET and DELETE on the endpoint return 405 by design.
POST https://api.tunnelpowered.com/api/mcp
Content-Type: application/json
Accept: application/json
Authorization: Bearer <token> # only needed for messaging tools
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18"}}3.The tools
| Tool | Auth | What it does |
|---|---|---|
search_businesses | None | Search by name, topic or place. Words are matched independently against name, location, description, offerings and FAQ; accents are ignored. |
get_business | None | Full profile for one slug: identity, contact, socials, offerings, FAQ, verification, machine-readable endpoints. |
check_merchant_verification | None | Live check of what a merchant is verified to and by whom — the level, the signed attestation, the expiry and the transparency-log position. |
contact_business | Bearer | Open a conversation. Returns a conversation id and a secret token — keep both. |
check_replies | Bearer | Poll a conversation for replies from the business. |
send_followup | Bearer | Send another message on an open conversation. |
Read verification.level rather than inferring anything from the presence of a record. "human" means a Tunnel employee checked identity, channel control and that the service is real; "automated" means machines proved only that the business controls the channels its record cites; null means neither, which is the common case. The separate humanReview object is about whether a person edited the knowledge-base entry — it is not a statement about the business.
Search matches words, not meanings. A business calling itself a *cofetărie* is not returned for bakery. An empty result set comes back as a normal answer with completeness "empty" and a hint, never as an error — it means "not in this index", which is not the same as "not in business".
4.The response envelope
Every tool result is wrapped. The wrapper exists so an agent never has to guess how much of what it is holding is real.
{
"tunnel_mcp": "1.0",
"tool": "get_business",
"retrieved_at": "2026-07-27T12:00:00.000Z",
"completeness": "partial",
"missing": ["offerings", "address"],
"freshness": { "source": "crawl", "observed_at": "…", "age_days": 3.2, "stale": false },
"data": { … }
}tunnel_mcp— schema version. Minor bumps are additive: ignore fields you do not recognise rather than failing on them.retrieved_at— when we answered. Not when the data was observed.freshness— when it *was* observed, how old that is in days, and whether we consider it stale.sourceiscrawl,record,index,liveorunknown; we sayunknownrather than defaulting to now.completenessandmissing—"full","partial"or"empty", with the absent fields named. An empty field means we do not hold that information, not that the business lacks it. If that distinction affects your answer, say which one you mean.
Partial data with stated gaps is returned in preference to a 404. An unknown slug comes back with candidate slugs in did_you_mean rather than a bare failure, because a 404 only tells an agent to give up.
5.Errors you can act on
A failed tool call returns the same envelope with an error object in place of data, and isError set on the MCP result. Branch on code, which is stable; the message is for a human reading a log.
code | What it means |
|---|---|
unknown_tool | No such tool. available_tools lists what does exist. |
missing_argument | A required argument was absent. required and accepted list the parameters. |
invalid_argument | A value was outside the allowed set. valid_values lists them. |
unknown_slug | Nothing is listed under that slug. did_you_mean carries candidates when there are any. |
auth_required | A messaging tool was called with no bearer token. Reading needs none. |
invalid_credentials | A token was sent and did not authenticate. We do not silently downgrade you to anonymous. |
rate_limited_ip, rate_limited_declared, rate_limited_agent | Slow down. retry_after_seconds says how long. |
internal_error | Our fault. Retry once, then tell us. |
Every error also carries fix: one imperative sentence naming the call to make instead. A refusal that does not tell you what would have worked is a bug on our side, not yours.
6.Registering your agent
Messaging a real business is rate-limited and attributable, so it needs a registered agent. Register once, then exchange the credentials for a token:
POST https://api.tunnelpowered.com/api/v1/agents/register → client_id, client_secret
POST https://api.tunnelpowered.com/api/v1/agents/token → access_token (client_credentials)Both endpoints are discoverable rather than hand-configured — we publish RFC 8414 authorization-server metadata and RFC 9728 protected-resource metadata:
GET https://api.tunnelpowered.com/.well-known/oauth-authorization-server
GET https://api.tunnelpowered.com/.well-known/oauth-protected-resource7.Opening a conversation
Send agent_name honestly — "Claude, on behalf of a user" is the right shape. The person on the other end decides how to reply based on who is asking, and a business that discovers it was talking to an undisclosed bot is a business that leaves.
POST https://api.tunnelpowered.com/api/kb/entities/{slug}/messages
POST https://api.tunnelpowered.com/api/kb/websites/{slug}/messages
{ "agent_name": "Claude, on behalf of a user",
"subject": "Table for four on Friday?",
"message": "…", // max 4000 characters
"reply_to": "user@example.com" } // optional, out of band
→ { "conversation_id": 123, "token": "…" } // the token is shown onceThen poll, and follow up on the same conversation:
GET https://api.tunnelpowered.com/api/kb/conversations/{id}?token=…
POST https://api.tunnelpowered.com/api/kb/conversations/{id}/messagesReplies are asynchronous and human. Poll on the order of minutes, not seconds, and tell your user the answer is coming from a person who may be asleep.
8.Checking verification
There are two levels and they are not interchangeable. Read the level field on the verification response rather than inferring anything from the presence of a badge.
level | Badge | What was actually established |
|---|---|---|
"human" | verifiedByAHuman | A Tunnel employee confirmed the identity of the person, their control of the channel, and that the service described is real. A second employee approved it. |
"automated" | verifiedAutomated | Machines confirmed the merchant controls the channels the record cites — a DNS record or file on the domain, a token in the public social bio, a code to the listed inbox. Nothing about identity or service reality. |
null | none | Neither. Treat the record as self-reported. |
Neither level is an endorsement of quality, solvency or licensing, and you should not present either as one. An "automated" response carries a limitations array — identity_not_verified, service_reality_not_verified, no_human_review — inside the signed payload, so the caveat travels with the signature rather than alongside it.
Every verification is a signed attestation with a position in a public transparency log, and the signing keys are published so you can verify offline:
GET https://api.tunnelpowered.com/.well-known/tunnel-trust.json # key id, rotation metadata
GET https://api.tunnelpowered.com/.well-known/jwks.json # the same keys as RFC 7517 JWKSFor anything high-stakes, treat a verification result older than five minutes as stale and re-check it live. Verification can be withdrawn.
To be clear about the state of the world: no AI vendor consults this registry today, because no cross-vendor trust registry exists. It is a signal your agent can choose to check, not one anybody checks for you.
9.Without MCP
Everything above is plain HTTP and works from curl. The read surface needs no credentials at all:
GET https://api.tunnelpowered.com/api/kb # index
GET https://api.tunnelpowered.com/api/kb/llms.txt # the guide, written for an LLM
GET https://api.tunnelpowered.com/api/kb/search?q={name}
GET https://api.tunnelpowered.com/api/kb/entities/{slug} # add ?format=md for markdown
GET https://api.tunnelpowered.com/api/kb/websites/{slug}10.Connect over A2A
The same three read skills are also reachable over Agent2Agent v1.0 (JSONRPC binding). The Agent Card is at the path the spec fixes:
GET https://api.tunnelpowered.com/.well-known/agent-card.json
POST https://api.tunnelpowered.com/api/a2a
{"jsonrpc":"2.0","id":1,"method":"SendMessage",
"params":{"message":{"role":"ROLE_USER",
"parts":[{"text":"cakes in Chisinau"}]}}}Two things trip up clients written against older material. The method is SendMessage — v0.3 called it message/send, and v1.0 renamed every operation; send the old name and you get a -32601 that names the new one. And a text part is {"text":"…"}, the protobuf oneof, not the v0.3 {"kind":"text","text":"…"}.
It returns a `Message`, never a `Task`. SendMessageResponse allows either, and these three skills are synchronous lookups — there is nothing to poll. So GetTask is not implemented, and answers -32601 explaining that rather than returning an empty task and letting you conclude yours was lost. Plain text is treated as a search; to choose a skill explicitly, send a data part such as {"skill":"get_business","slug":"…"}.
Read-only, on purpose. Messaging a business is not exposed over A2A. An agent reaching a merchant on the strength of a self-declared identity would be committing a person to a conversation on a claim nobody checked; that waits until there is a mandate model behind it. Reads are safe to open to anyone, and they are.
The card is unsigned. Cards *may* carry a JWS over the JCS canonicalization and we already publish a JWKS, but a subtly wrong canonicalization produces a signature that fails verification, which looks like tampering and is worse than none. The signatures field is absent rather than empty, because an empty array would claim we signed it zero times.
11.The API as a contract
The whole public surface is also described as OpenAPI 3.1, served from the origin it describes so that a cached copy of this site can never disagree with the live API:
GET https://api.tunnelpowered.com/openapi.jsonIt covers exactly what this page covers, and nothing else: the knowledge base, the landline, verification and the transparency log, agent registration, and the MCP endpoint. Endpoints absent from it are internal and may change without notice. That boundary is deliberate — describing an endpoint in a machine-readable contract is an undertaking to keep it working, and we only want to make that undertaking about the surface we already ask you to depend on.
The per-website artifact routes under /api/public/ are excluded for a different reason: they need a ?t= token that belongs to a customer, so an agent has no way to call them and calling them "public" would be misleading.
The MCP endpoint appears as a single path. Its tools, their arguments and the response envelope are described by initialize and tools/list at runtime rather than copied into the spec, because a second copy is a second thing that can drift.
In the official MCP registry the server is com.tunnelpowered/knowledge-base. Verify the identifier there rather than trusting a descriptor file that claims it.
12.Rate limits and etiquette
Public endpoints are rate-limited per IP in a fixed window. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a refusal carries Retry-After in seconds. Read those headers rather than guessing — the limit is configurable and we will not silently change behaviour under you.
The MCP endpoint meters reads on three keys. The IP ceiling is the hard one and is checked first. Inside it, a self-declared identity — an MCP clientInfo.name or a descriptive User-Agent — gets its own narrower share, so one noisy integration cannot consume an address's whole budget. A registered agent holding a bearer token gets a higher ceiling of its own.
Declaring who you are never raises a limit — only proving it does. A name you type about yourself is not checkable, so it buys you nothing and costs you nothing; there is no incentive either way. Please send one anyway: it is how we tell agent traffic apart from noise when we report on who uses this.
Within tools/call, a rate-limit refusal comes back as a normal tool result with isError and a rate_limited_* code, not as an HTTP error — a model driving the tool reads the result, not the status line. Other methods get a JSON-RPC error and a 429.
Three things we ask of any agent using the landline: identify yourself, do not open a conversation you will not read the reply to, and do not use it to broadcast. A business that gets three useless agent messages will stop answering the fourth, and the channel is only worth having if the humans on it stay engaged.
13.Disputes
If an interaction goes wrong — a business misrepresents itself, or a verification looks incorrect — an authenticated agent can contest it:
POST https://api.tunnelpowered.com/api/kb/conversations/{id}/disputeA dispute is reviewed by a person. It is the mechanism that keeps verification from being a rubber stamp, so please use it.
Questions, or a use case that does not fit: contact@tunnelpowered.com. Related reading: how the visibility score is computed.