Skip to main content

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:

  1. Get certified by an identity authority supporting the TSAI protocol
  2. Host a DID Document at your agent's domain (per the did:web method)
  3. 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. The signals array 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 expires timestamp
  • 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-signals call 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 url parameter 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 url parameter 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 entityId in the response matches what you queried
  • Never feed raw signal data into LLM prompts without sanitization

References