The one rule
Trust the registry you already know, never one a page points you to.
A site saying it's verified is only a claim. Pin
https://registry.fofo.network in your agent's configuration and verify
against it.
Verification checks the business's own signature on its profile, the registry's signature on each fact, the status lists, the delegation that lets the registry serve the record, and the DID document and DNS record on the domain. The level is recomputed from the signed facts, never read from a label.
MCP server
Streamable HTTP at https://registry.fofo.network/mcp. It offers three
tools, and none of them takes a registry address, so a page can't redirect your agent
to a different registry.
| Tool | What it does |
|---|---|
lookup_business |
Finds a business by domain or phone and returns its listing (level, domains,
phones), with only the registry's signature checked. Call
verify_business before relying on it.
|
verify_business |
Runs the full verification for a domain: signatures, status, delegation and
the two-way domain binding. Returns verified, the level, each
fact and a one-line summary.
|
verify_presentation |
Checks a key-bound presentation another party gave you, for example an agent proving it acts for a business through a delegation chain. Use a nonce you generated yourself and your own origin as the audience. |
Verifiers
The same checks, in your own code. Both replay the same shared test vectors.
import { verifyBusiness, FFN_BUSINESS_REGISTRY } from '@fofonet/verify-business';
const r = await verifyBusiness('acme-hvac.com', { registry: FFN_BUSINESS_REGISTRY });
if (r.verified) console.log(r.level, r.levelName); // 2 'Licensed and insured'
console.log(r.summary);
from ffn_verify import verify_business
REGISTRY = {"origin": "https://registry.fofo.network", "name": "Sweet Papa Technologies LLC"}
result = verify_business("acme-hvac.com", registry=REGISTRY)
if result["verified"]:
print(result["level"], result["levelName"])
HTTP API
Free and rate-limited. All responses are public facts; private evidence never leaves the registry.
| Request | Returns |
|---|---|
GET /v1/lookup?domain=… |
The signed record for the business that owns the domain, or 404. |
GET /v1/lookup?phone=… |
The signed record for the business whose verified phones include the number, or 404. |
GET /v1/businesses/{id} |
The signed record by its ID. |
GET /v1/businesses/{id}/evidence |
JSON: each fact's state and reason, and each check's source, result and date. |
GET /v1/audit?domain=… |
The agent-readiness audit as JSON (also at /audit as a page).
|
GET /.well-known/did.json |
The registry's DID document, with the key that signs records and facts. |
The signed record
-
A JWT with media type
application/ffn-business-record+jwt, signed by the registry and valid for two days. - It carries the profile the business signed, the delegation that lets the registry publish it, the registry's attestations (entity, official channels, license, insurance, representative) and recent revocations with reasons.
- Each attestation carries its checks: what was checked, against which source, by an automated check or a reviewer, the result and the date.
- Facts can be suspended (a lapsed binding or insurance) or withdrawn, always with a reason code a verifier can read.
Domain binding
Each listed domain points back to the registry entry in two ways:
-
https://<domain>/.well-known/did.json, a DID document listing the business key, with a service entry of typeFfnVerifiedBusinessnaming the registry record. A secondary domain names the primary DID inalsoKnownAs. -
A DNS TXT record at
_ffn.<domain>:v=ffn1; did=<did>; registry=<registry host>.
A business with only the DNS record is listed with a lower-assurance binding, and verifiers report it.
Pages for people
Every listed business has a public profile at /b/<domain>, with the
same facts in schema.org JSON-LD for crawlers. It points to the signed record, which
is the thing to verify. The sitemap lists every profile.