3webs Visibility Engine
3webs audits whether a domain is addressable by autonomous agents. It would be indefensible to sell that and not be addressable ourselves. So this page is not documentation of an intention — every URL below returns a real response right now, and you can run each example without asking us for anything.
An agent that has never heard of us needs one thing to start: a description of what we can do, in a place it already knows to look. That place is /.well-known/agent-card.json. Ours is public, unauthenticated and served with CORS open.
{
"name": "3webs OBS",
"description": "Observes how a domain appears across the three webs...",
"version": "3.0.0",
"supportedInterfaces": [
{ "url": "https://3webobs.com/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" },
{ "url": "https://3webobs.com/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "0.3" }
],
"capabilities": {
"streaming": false,
"pushNotifications": false,
"extensions": [ { "uri": "https://3webobs.com/ext/3webs-observation/v1", ... } ]
},
"securitySchemes": {},
"securityRequirements": [],
"defaultInputModes": ["application/json", "text/plain"],
"defaultOutputModes": ["application/json", "text/plain"],
"skills": [ { "id": "obs_one_shot", ... }, ... ]
}
Two interfaces, one URL. The same endpoint is declared twice, at protocol version 1.0 and 0.3. The specification allows an agent to expose several interfaces for the same transport at different versions, and the first entry is the preferred one. This is not a compatibility hack bolted on afterwards — it is how the standard says to carry two generations of clients at once.
securityRequirements, not security. The field was renamed in v1. An agent card that still carries security while declaring 1.0 is describing itself in a vocabulary the version does not have. Both arrays are empty here because the skills below are open: there is nothing to authenticate yet, and pretending otherwise would be theatre.
Custom metadata lives in capabilities.extensions. We publish things the standard has no field for — our role in the ADI network, the determinism guarantee, the EU AI Act disclosure. Those belong in the sanctioned extension point, identified by a URI we own, not as invented top-level keys. A parser that does not know our extension ignores it cleanly instead of choking on it.
What we removed. The card used to advertise /audit as an HTTP+JSON interface. It is a perfectly good REST endpoint, but it is not the A2A HTTP binding — that binding has prescribed URL patterns such as /v1/message:send. Declaring it as one meant any conforming client that trusted the card would fail on the first call. It is now documented as what it is: our own REST API, in section 5.
The two versions differ in ways that break a naive parser, so the client has to say which one it speaks. It does that with a header.
A2A-Version | Interpreted as | Response shape |
|---|---|---|
1.0 | v1.0 | role: "ROLE_AGENT", parts carry no kind |
0.3 | v0.3 | role: "agent", parts carry kind: "data" |
| absent | v0.3 | legacy shape |
| anything else | rejected | -32003 VersionNotSupportedError |
An absent header means 0.3, not "the newest available". That is counter-intuitive and it is what the specification requires: clients written before the header existed must keep working unchanged. Defaulting to 1.0 would silently break every one of them.
| Concept | v0.3 | v1.0 |
|---|---|---|
| Method name | message/send | SendMessage (both accepted) |
| Role | "user" / "agent" | ROLE_USER / ROLE_AGENT |
| Part discriminator | kind: "text" / "data" | none — a part is identified by the field it carries |
| Task state | "working" | TASK_STATE_WORKING |
The third row is the one that bites. In v1 the kind discriminator was removed: a text part is simply {"text": "..."} and a data part is {"data": {...}}. A server that still looks for kind will find nothing in a valid v1 request, conclude the parameters are empty, and answer -32602 Invalid params. Ours did exactly that until an independent audit caught it — see section 7.
The capability: request an audit via agent. Skill id obs_one_shot. An agent hands us a URL and receives a scored, evidence-backed observation of that domain across the three webs. Synchronous — the report comes back in the same response, there is no task to poll.
POST https://3webobs.com/a2a
Content-Type: application/json
A2A-Version: 1.0
{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "6f1c0b2e-9a11-4f3d-8c77-2b0a5d9e4471",
"role": "ROLE_USER",
"parts": [
{ "data": { "skill": "obs_one_shot", "url": "https://example-retail.eu" } }
]
}
}
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"messageId": "b48d5c31-7e02-4a19-9f6b-1d3c8e0a7752",
"role": "ROLE_AGENT",
"parts": [
{
"mediaType": "application/json",
"data": {
"skill": "obs_one_shot",
"mode": "one-shot",
"observationId": "obs_m4x9k2p1",
"url": "https://example-retail.eu/",
"global": 64,
"three_webs": { "human_web": 83, "ai_web": 60, "machine_web": 41 },
"dimensions": { "AI_SIGNALS": 41, "AEO": 68, "GEO": 52, "AIO": 61, "SEO": 83 },
"tested": 129,
"not_applicable": 38,
"signals": { "AI_SIGNALS": [
{ "id": "ai3", "n": "llms.txt present and valid", "status": "fail", "w": 5,
"method": "/llms.txt returned HTTP 404. No equivalent declaration found." }
] },
"plan": {
"critical": { "title": "Fix first", "items": ["llms.txt present and valid", "..."] }
},
"determinism": "rule-based; no score generated by a language model"
}
}
]
}
}
Three fields carry the weight. observationId is the handle for a later before-and-after comparison, so an agent that fixes something can prove it worked. tested and not_applicable let it reason about coverage instead of trusting a bare number. Every signal carries method — the evidence that produced the verdict — so a downstream system can quote the finding rather than assert it.
If your client predates v1, change nothing except what you already send. Omit the header and use the legacy shapes; the response comes back in the legacy shape too.
{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"kind": "message",
"role": "user",
"messageId": "t1",
"parts": [ { "kind": "data", "data": { "skill": "obs_one_shot", "url": "https://example-retail.eu" } } ]
}
}
}
All five are declared in the agent card and all five are dispatched from the same SendMessage call. The skill is selected by the skill field inside the data part.
| Skill | Parameters | Returns |
|---|---|---|
obs_one_shot |
url · pagespeed (optional, false to skip) |
Full observation, scores, per-signal evidence, action plan, observationId |
obs_catalogue |
none | Every signal in the catalogue with id, name, category and weight, grouped by dimension and by web |
obs_explain |
url · signal_id |
One signal: verdict, score and the evidence behind it. Use before acting on a failure |
obs_diff |
url · baseline_id |
Which signals changed since a stored observation, in which direction, with evidence |
obs_permanent |
url · interval · notify · threshold · or action: "cancel" with subscriptionId |
A registered subscription, readable and cancellable through GetTask and CancelTask |
{ "data": { "skill": "obs_explain", "url": "https://example-retail.eu", "signal_id": "ai3" } }
{ "data": { "skill": "obs_diff", "url": "https://example-retail.eu", "baseline_id": "obs_m4x9k2p1" } }
Subscriptions follow the task lifecycle rather than a private convention: GetTask reads one, CancelTask ends it. An agent that knows A2A already knows how to manage them.
Nothing below needs a key. The first three observations per domain each month are free.
curl -X POST https://3webobs.com/a2a \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":
{"messageId":"t1","role":"ROLE_USER",
"parts":[{"data":{"skill":"obs_catalogue"}}]}}}'
import requests
r = requests.post(
"https://3webobs.com/a2a",
headers={"A2A-Version": "1.0"},
json={
"jsonrpc": "2.0", "id": 1, "method": "SendMessage",
"params": {"message": {
"messageId": "t1", "role": "ROLE_USER",
"parts": [{"data": {"skill": "obs_one_shot",
"url": "https://example.com"}}],
}},
},
)
report = r.json()["result"]["parts"][0]["data"]
print(report["global"], report["three_webs"], report["observationId"])
$body = @{
jsonrpc = "2.0"; id = 1; method = "SendMessage"
params = @{ message = @{
messageId = "t1"; role = "ROLE_USER"
parts = @(@{ data = @{ skill = "obs_catalogue" } })
}}
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri https://3webobs.com/a2a -Method Post `
-ContentType "application/json" `
-Headers @{"A2A-Version"="1.0"} -Body $body
If you are writing ordinary integration code and do not want a JSON-RPC envelope, the same engine is reachable directly. This is our own API, not an A2A binding, and the agent card no longer claims otherwise.
| Endpoint | Body |
|---|---|
POST /audit | { "url": "..." } |
POST /explain | { "url": "...", "signal_id": "ai3" } |
POST /diff | { "url": "...", "baseline_id": "obs_..." } |
GET /stats | — service metadata and audit counter |
GET /signals | — the signal catalogue |
A capability list is only credible if it also says where it stops.
No streaming. message/stream is absent and capabilities.streaming is false. Every skill completes inside a single SendMessage, so there is no partial state to stream. Declaring streaming and then returning one chunk would be a lie with extra steps.
No HTTP+JSON binding. The REST binding is optional in v1. We expose JSON-RPC properly rather than a half-built second transport, and we do not list our REST API as something it is not.
We do not invoke other agents just because they invite us. When our scanner meets a domain whose capabilities.json declares safe_to_invoke: true, it no longer posts to that domain automatically. That declaration is made by the site being audited, about itself — any site can write it, and acting on it means executing something on a target that granted itself permission. Invocation is now opt-in by whoever runs the audit. Observation is passive by default.
No authentication yet. securityRequirements is empty because the skills are open. When that changes, the card changes with it, in the same release.
On 31 August 2026 this implementation was audited by a third party that did not take the card's word for anything. It was right about three things, and publishing what it found is more useful than publishing a badge.
| Finding | Status |
|---|---|
The card declared A2A 1.0 while the server still parsed v0.3 data shapes. A conforming v1 request failed with -32602. | Fixed — parts are detected by the field they carry, responses are emitted in the negotiated shape |
/audit was declared as an HTTP+JSON A2A interface. It is a proprietary REST endpoint. | Fixed — removed from supportedInterfaces, documented in section 5 |
The card used security and a custom x-3webs top-level key. | Fixed — securityRequirements, metadata moved to capabilities.extensions |
Scheduled re-observation for obs_permanent was declared available while no scheduler was running. | Open — the skill description now says so, and the pricing page marks the plans accordingly |
The uncomfortable part is the general lesson. An agent card is a claim, and claims are cheap. What makes the machine web work is that any client can check the claim in one request — which is exactly what we do to other domains, and exactly what was done to us. If you take one thing from this page: audit your own card with a conforming client before you publish it.
It measures how a website presents itself to AI systems, answer engines and search crawlers. The engine fetches the page, robots.txt, sitemap.xml and the declared machine-readable signal files, then evaluates 167 signals across the human web, the AI web and the machine web.
No. Every score is produced by deterministic rules applied to evidence actually observed on the website. A language model writes only the short narrative summary. The same website in the same state always produces the same score.
Because a signal that cannot be verified from public sources has not failed — it simply could not be tested. Marking it as failed would invent a verdict. Every report states how many signals were actually tested.
SEO is the technical and content foundation used by search engines. AEO structures content so assistants extract direct answers. GEO makes the brand entity discoverable and citable by generative systems. AIO covers how AI systems understand, rank and reproduce content across the full stack.
No. The audit inspects only publicly served resources. Nothing is installed, no tracking code is added, no authentication is attempted, and no private area is accessed.
The first three reports per domain each calendar month are free. After that each report is EUR 19, excluding VAT. Monitoring one domain costs EUR 29 per month for a monthly interval, EUR 49 for weekly and EUR 99 for daily. API access is EUR 19 per call, with volume packs at EUR 12.90 and EUR 9.90 per call and an enterprise tier on request. Full pricing is published at 3webobs.com/pricing.
Yes. The engine implements the Agent-to-Agent protocol at /a2a, in versions 1.0 and 0.3, with an agent card at /.well-known/agent-card.json. A REST endpoint at /audit accepts a URL and returns the same report as JSON. Worked examples are published at 3webobs.com/agents.
No. Third-party AI systems change their behaviour without notice and no provider guarantees citation. The audit identifies observable technical signals within your control; it does not promise a ranking, a citation or a commercial outcome.
No. The report is a technical observation, not a certification and not a legal opinion. For a formal classification under Regulation (EU) 2024/1689, obtain independent legal analysis.
AIVENTURE S.R.L., a company registered in Bucharest, Romania, European Union, operating under the 5thElement.ai brand. Registration details and the full legal documentation are published on the site.