Integration Guide — Agent Developers
This guide explains how agent developers integrate with the #trstd <protocol> to assess the trustworthiness of websites and services.
This document is non-normative. For requirements, see the specification documents in architecture/.
Step 1: Discover the Trust Verification Tool
An agent typically arrives at a specific URL — for example, https://www.example.org/de/products/123. This URL drives the entire trust verification flow.
Discovery
Scan the HTML <head> for a <link rel="trstd-protocol"> tag:
The link tag provides the verify endpoint URL with a pre-computed entityId for the page. The agent appends the page URL and context, then makes a single HTTP GET. No MCP server, no tool discovery roundtrips.
Critical: the agent MUST construct the url parameter from the actual page URL it is visiting — never from the link tag or any other page-provided value. A malicious site could embed a link tag with a url parameter pointing to a legitimate, high-trust site. If the agent used that value, the authority would return trust signals for the wrong site, and the agent would incorrectly trust the malicious page. By constructing the url from its own navigation state, the agent ensures the verification matches the page it is actually on.
Step 2: Query the Verification Endpoint
The agent calls the HTTP verify endpoint directly. The link tag's href already contains the endpoint URL with the entityId as its final path segment. The agent appends ?url=...&context=... and makes a GET request. The trust authority validates that the URL actually falls within that entity's scope — this server-side check prevents agents from sending a mismatched entity.
Agent identification is optional per-request. Authorities MUST NOT gate access on it. Agents MAY present a did:web JWT (see Identified requests below) if they have one.
Unidentified request:
// The URL the agent is visiting — this is the same URL used for entity matching
const visitedUrl = "https://www.example.org/de/products/123";
// The entityId from the matched entity
const entityId = "d6f2fdf4-f829-4ce6-a1cc-e2bd957709db";
// Query the trust authority with the visited URL — entityId is the path segment
const params = new URLSearchParams({ url: visitedUrl, context: "purchase" });
const response = await fetch(
`https://trust-authority.example.org/v1/entities/${entityId}/trust-signals?${params}`,
{
headers: {
Accept: "application/json",
},
}
);
const trustData: VerifyResponse = await response.json();
Identified Requests
An agent that wants to identify itself to the authority needs a did:web identity. This is optional — authorities accept unidentified requests — but identified agents may receive logging, relaxed rate limits, or tailored responses (implementation-defined per authority). This requires:
- Get certified by an identity authority supporting the TSAI protocol
- Host a DID Document at your agent's domain (per the
did:webmethod) - The DID Document contains your agent's public key
Example DID Document at https://agent.example.org/.well-known/did.json:
{
"@context": "https://www.w3.org/ns/did/v1",
"id": "did:web:agent.example.org",
"verificationMethod": [{
"id": "did:web:agent.example.org#key-1",
"type": "Ed25519VerificationKey2020",
"controller": "did:web:agent.example.org",
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}],
"authentication": ["did:web:agent.example.org#key-1"]
}
Identified request:
import { SignJWT, importPKCS8 } from "jose";
// The URL the agent is visiting — this is the same URL used for entity matching
const visitedUrl = "https://www.example.org/de/products/123";
// The entityId from the matched entity
const entityId = "d6f2fdf4-f829-4ce6-a1cc-e2bd957709db";
// Create the identification JWT
const privateKey = await importPKCS8(privateKeyPem, "EdDSA");
const token = await new SignJWT({
iss: "did:web:agent.example.org",
aud: "trust-authority.example.org",
})
.setProtectedHeader({ alg: "EdDSA", kid: "key-1" })
.setIssuedAt()
.setExpirationTime("1h")
.sign(privateKey);
// Query the trust authority with the visited URL — entityId is the path segment
const params = new URLSearchParams({ url: visitedUrl, context: "purchase" });
const response = await fetch(
`https://trust-authority.example.org/v1/entities/${entityId}/trust-signals?${params}`,
{
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
}
);
const trustData: VerifyResponse = await response.json();
Step 3: Verify the Response Signature
Before processing any signals, verify the authority's signature:
import { createRemoteJWKSet, importJWK } from "jose";
import canonicalize from "canonicalize"; // RFC 8785 JCS implementation
import { verify } from "@noble/ed25519"; // or any Ed25519 library
// Fetch the authority's public keys (cache this)
const jwksResponse = await fetch("https://trust-authority.example.org/.well-known/jwks.json");
const jwks = await jwksResponse.json();
// Select the key matching the kid field in the response
const { kid, signature, ...responseBody } = trustData;
const jwk = jwks.keys.find((k: any) => k.kid === kid);
// JCS-canonicalize the response body (excluding signature, including kid)
const payload = canonicalize({ ...responseBody, kid });
const payloadBytes = new TextEncoder().encode(payload);
// Decode the base64url signature and verify
const sigBytes = base64urlDecode(signature);
const publicKey = await importJWK(jwk, "EdDSA");
const valid = await verify(sigBytes, payloadBytes, publicKey);
if (!valid) throw new Error("Signature verification failed");
// Bind the response to the request that produced it: recompute the
// canonical URL the same way the authority does and confirm meta.url
// and meta.context match what was sent.
function canonicalUrl(raw: string): string {
const u = new URL(raw);
u.username = "";
u.password = "";
u.search = "";
u.hash = "";
return u.toString();
}
if (trustData.meta.url !== canonicalUrl(visitedUrl)) {
throw new Error("Response URL does not match request");
}
if (trustData.meta.context !== "purchase") {
throw new Error("Response context does not match request");
}
Always verify before processing. Reject responses with invalid signatures. Validate your JCS implementation against the normative test vectors before deployment.
The meta.url/meta.context checks close a contextual-replay attack where a cached signed response for one URL within an entity's scope is substituted for another. See ADR-007 — Binding the Signed Response to the Request.
Step 4: Evaluate Trust Signals
Process the signals according to your agent's trust policy:
// Reference signal types have typed data schemas.
// Authorities may define additional types — use CustomSignal for those.
type TrustSignal =
| IdentitySignal
| ReputationSignal
| ComplianceSignal
| RecourseSignal
| ContactSignal
| CustomSignal;
interface IdentitySignal {
type: "identity";
verifiedAt: string;
data: { legalName: string; country: string; registrationNumber?: string; registrationAuthority?: string; incorporationDate?: string; memberSince?: string };
}
interface ReputationSignal {
type: "reputation";
verifiedAt: string;
data: { aggregateRating: number; reviewCount: number; sourceCount: number; ratingDistribution?: { oneStar?: number; twoStar?: number; threeStar?: number; fourStar?: number; fiveStar?: number } | null; periodMonths?: number };
}
interface ComplianceSignal {
type: "compliance";
verifiedAt: string;
data: { certifications: string[]; lastAudit?: string; auditor?: string; nextAudit?: string };
}
interface RecourseSignal {
type: "recourse";
verifiedAt: string;
data: { mechanism: string; provider: string; responseTimeDays?: number; url?: string };
}
interface ContactSignal {
type: "contact";
verifiedAt: string;
data: { emailVerified: boolean; phoneVerified: boolean; addressVerified: boolean; supportUrl?: string };
}
interface CustomSignal {
type: string;
verifiedAt: string;
data: Record<string, unknown>;
}
interface AssessmentExtensionField {
value: string | number | boolean | null;
description: string;
}
interface Assessment {
action: "proceed" | "caution" | "decline";
reasoning: string;
highlights?: string[];
extensions?: Record<string, AssessmentExtensionField>;
}
type VerificationStatus = "verified" | "lapsed" | "revoked" | "pending";
interface ResponseMeta {
responseId: string;
entityId: string;
status: VerificationStatus;
url: string; // canonical scheme + host + path the authority validated
context?: string; // echoes the agent's context query parameter; omitted if none was sent
timestamp: string;
expires: string;
}
interface VerifyResponse {
meta: ResponseMeta;
signals: TrustSignal[];
assessment?: Assessment;
signature: string;
}
function assessTrust(trustData: VerifyResponse): string {
if (trustData.meta.status !== "verified") {
return trustData.meta.status; // "lapsed", "revoked", or "pending"
}
const signals = new Map(
trustData.signals.map((s) => [s.type, s])
);
// Check identity
const identity = signals.get("identity");
if (!identity) {
return "insufficient_identity";
}
// Check reputation
const reputation = signals.get("reputation");
if (reputation) {
const rating = reputation.data.aggregateRating as number;
const count = reputation.data.reviewCount as number;
if (rating < 3.0 || count < 10) {
return "low_reputation";
}
}
// Check compliance (optional, depends on your requirements)
const compliance = signals.get("compliance");
// Check recourse (optional)
const recourse = signals.get("recourse");
return "trusted";
}
This is a simplified example. Production agents should implement richer policies that account for verification freshness and the specific transaction context.
Using the Assessment (Fast Path)
When the response includes an assessment, agents can use it as a fast path and fall back to signal-level evaluation for more detail:
function decideTrust(trustData: VerifyResponse): string {
if (trustData.meta.status !== "verified") {
return trustData.meta.status; // "lapsed", "revoked", or "pending"
}
// Fast path: use the authority's assessment if available
if (trustData.assessment) {
const { action } = trustData.assessment;
if (action === "proceed") {
return "trusted";
}
if (action === "decline") {
return "untrusted";
}
// action === "caution" — fall through to signal-level evaluation
}
// Detailed path: evaluate individual signals
return assessTrust(trustData);
}
The authority also includes a context-specific field whose name reflects the agent's intent — for example, safeToPurchase when context=purchase. LLM agents can use the field name itself as a semantic signal: "safeToPurchase": "yes" is self-explanatory. The action enum and the context-specific field coexist in the same assessment object.
The assessment.reasoning and assessment.highlights fields contain natural language suitable for LLM consumption. Sanitize these fields before including them in prompts — treat them as authority-generated summaries, not trusted instructions.
Step 5: Handle Edge Cases
The verify endpoint returns a status enum with four values, plus a 404 for unknown entities:
"verified"(200) — the entity is actively verified. Thesignalsarray may be empty if the entity was recently onboarded and signal data has not yet been collected."lapsed"(200) — verification expired (e.g., expired compliance). Treat with more caution than a verified entity, but with more nuance than an unknown one — the provider was previously verified."revoked"(200) — verification withdrawn (e.g., fraud or policy violation). Treat as a negative signal — the authority actively removed trust."pending"(200) — application received, not yet confirmed. The provider is in the verification pipeline but has no trust signals yet.- 404
entityNotFound— the entity is unknown to the authority. Treat as unknown trust.
Entity not found (404): The service is not registered with the trust authority. Error responses are unsigned — a man-in-the-middle can forge a 404 to suppress a valid entity. The protocol requires agents to retry at least once and prefer cached signed responses before acting on the error. If retries fail and no cache exists, treat the result as "trust unknown" (failed evaluation), not "entity untrusted." For high-value contexts, try a secondary network path before accepting the result.
Entity mismatch (400): The URL does not fall within the entity's scope. The link tag on the page contains an incorrect entityId — the agent treats this as a discovery failure and stops.
Authority unreachable: Use cached responses if they have not expired. If no valid cache exists, fall back to your agent's default policy.
Rate limited (429): Respect the Retry-After header. Consider caching responses more aggressively.
Unknown signal types: Ignore signal types your agent does not recognize. Do not fail on unknown types — this ensures forward compatibility with extensions.
Implementation Recommendations
Caching
- Cache trust authority responses until their
expirestimestamp - Cache the authority's JWKS for up to 1 hour
- Implement cache invalidation on signature verification failure (the authority may have rotated keys)
Error Handling
- Treat signature verification failure as "no trust data available" — never fall through to using unverified data
- Log all verification failures for debugging
- Implement circuit breakers for authority connectivity issues
Performance
- A single
/v1/entities/{entityId}/trust-signalscall per entity is sufficient — do not poll - Batch discovery checks at the start of a session rather than per-page
- Parallelize trust checks when evaluating multiple entities
Security
- Always construct the
urlparameter from the agent's own navigation state — never from values in the link tag, page content, or any other source the page controls. This is the primary defense against trust-borrowing attacks where a malicious page points verification at a legitimate site. - The
urlparameter sends the full URL to the authority, including query parameters. Query parameters may contain session tokens or user-specific context. Review what your URLs contain before sending them — the authority can observe the full URL even though entity matching uses only hostname and path. - Maintain an allowlist of trusted authority domains — reject link tags whose endpoint domain is not on the list (per the spec, this is the primary defense against authority impersonation)
- When using authenticated requests, store agent private keys in a secure enclave or HSM
- Use short-lived JWTs (1 hour or less) for authenticated requests
- Validate that
entityIdin the response matches what you queried - Never feed raw signal data into LLM prompts without sanitization
References
- Discovery specification — Link tag and authority allowlist
- Verification API — Endpoint behavior, request/response format
- Trust Signals — Signal types and envelope structure
- Security — Signatures and anti-gaming
- OpenAPI Specification — Machine-readable API contract
- Roadmap — Planned future work (manifest, MCP, transparency log); see also the integration preview