Skip to content

Websites quickstart

A website doesn’t need a Bage account or a client ID to accept agents. It needs two things: a file that tells agents it accepts Bage, and a check on each request.

Serve this at /.well-known/agent-login, with your own origin as audience:

{
"audience": "https://shop.example",
"providers": [{ "name": "Bage", "issuer": "https://api.trybage.com" }],
"token_header": "agent-token",
"signature": "web-bot-auth"
}
server.ts
// npm install @trybage/sdk
import http from "node:http";
import { createVerifier } from "@trybage/sdk/site";
const origin = process.env.ORIGIN ?? "https://shop.example"; // your site, as agents see it
const bage = createVerifier({ issuers: [process.env.BAGE_ISSUER ?? "https://api.trybage.com"], audience: origin });
const send = (response: http.ServerResponse, status: number, body: object) => {
response.writeHead(status, { "content-type": "application/json" });
response.end(JSON.stringify(body));
};
async function handle(request: http.IncomingMessage, response: http.ServerResponse) {
// Tells agents that this site accepts Sign in with Bage.
if (request.url === "/.well-known/agent-login") return send(response, 200, bage.discovery());
try {
const who = await bage.verify({
method: request.method ?? "GET",
authority: request.headers.host ?? "",
path: request.url ?? "/",
headers: request.headers,
});
if (!who) return send(response, 403, { error: "This page needs Sign in with Bage" }); // no agent token: an ordinary bot or visitor
if (!who.scopes.includes("browse")) return send(response, 403, { error: "browse wasn't approved" });
return send(response, 200, { hello: `${who.agent.name} (${who.agent.operator}), acting for ${who.userName ?? who.userId}` });
} catch (error) {
// A bad, expired or revoked token, or a request not signed with the agent's key.
return send(response, 401, { error: error instanceof Error ? error.message : "invalid agent" });
}
}
http.createServer((request, response) => void handle(request, response)).listen(Number(process.env.PORT ?? 3000));

verify gives you the agent (agent.name, agent.operator), the person (userId, the same each time, and userName) and what they allowed (scopes). The verifier fetches Bage’s public keys every 5 minutes and the revoked list every 15 seconds, so when a person cuts off an agent, it’s cut off on your site within seconds.

Without the SDK: check the token as a JWT (typ: agent+jwt, EdDSA) against the keys at https://api.trybage.com/.well-known/jwks.json, with your origin as aud. Reject any jti listed at /v1/revoked, then check the request signature with the key in cnf.jwk. Or call POST /v1/introspect with the token.

Every token says who the person is: userId is the same each time. To find their existing account on your site, agents ask for the email scope and the token carries the person’s verified email, which the person sees on the approval screen. The SDK returns it as userEmail. Every site accepts email, since without it a site can’t match anyone.

Optionally, start free and add your site in the console to choose which scopes agents may ask for, let in only the agents you allow, cut one off, and see every agent sign-in. Settings apply once you prove you run the domain, with a file at /.well-known/bage-site-verification.txt or a TXT record at _bage-verification.yourdomain.com.

A new site starts with nothing turned on, so agents can only ask for email. In the console’s Scopes card you add what agents may do on your site:

  • Your own scopes. Name it the way your agents will ask for it (returns, gift_cards: lowercase letters, digits and _, 2 to 40 characters), give it a title and a line on what it lets an agent do. People see that title and description, marked as your site’s own, when they approve.
  • Standard scopes, Bage’s shared ones (browse, cart, checkout, read_account, messages, bookings), with one click.

Agents can’t ask your site for anything else (invalid_scope), and your own scopes mean nothing on other sites. Removing a scope ends the sign-ins that carry it at their next refresh. Your scopes appear in the token’s scope (and the SDK’s scopes), and in the site list so agents can see what to ask for.

For pages rather than APIs, turn the agent into your normal logged-in session once, and hear from Bage when it’s revoked: see Sessions, revocation and the site list.