Skip to content

Commit cd455bc

Browse files
author
S. K. Rotwang MMMMMMMMM
committed
feat: serve /llms.txt from the API origin (agent crawler discovery)
1 parent b7e2fec commit cd455bc

3 files changed

Lines changed: 40 additions & 0 deletions

File tree

docs/llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Seneschal lets a receiver get an HMAC-signed webhook the instant a Monero (XMR)
1313
- Free UFVK derivation from a seed phrase (rate-limited): POST https://api.seneschal.space/v1/private/derive-viewkey
1414

1515
## API discovery
16+
- OpenAPI 3.1 document (all routes, x402 prices per route): https://api.seneschal.space/openapi.json
1617
- x402 payment manifest: https://api.seneschal.space/.well-known/x402
1718
- Health + table sizes: https://api.seneschal.space/v1/health
1819
- MCP server (Streamable HTTP transport): https://mcp.seneschal.space/

src/rest-server.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,29 @@ export async function buildApp(options = {}) {
632632
return reply.send(faviconCache);
633633
});
634634

635+
// `/llms.txt` — the llmstxt.org convention agent crawlers fetch from
636+
// the API origin (the docs site serves its own copy, but indexers
637+
// that found us via openapi.json look here, not on the docs host).
638+
// Same lazy-load-and-cache pattern as the favicon.
639+
let llmsTxtCache;
640+
app.get('/llms.txt', async (req, reply) => {
641+
if (llmsTxtCache === undefined) {
642+
try {
643+
llmsTxtCache = await readFile(new URL('../docs/llms.txt', import.meta.url), 'utf8');
644+
}
645+
catch {
646+
llmsTxtCache = null;
647+
}
648+
}
649+
if (!llmsTxtCache) {
650+
reply.code(404);
651+
return { error: { code: 'not_found', message: 'no llms.txt' } };
652+
}
653+
reply.header('content-type', 'text/plain; charset=utf-8');
654+
reply.header('cache-control', 'public, max-age=3600');
655+
return reply.send(llmsTxtCache);
656+
});
657+
635658
// `/.well-known/x402` — opt-in discovery manifest matching the
636659
// emerging convention that x402 service-discovery agents (e.g.
637660
// Agorion, coinbase/x402 #1379) scrape from a provider's apex

test/rest-server.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ describe('routing & shape', () => {
109109
expect(r.statusCode).toBe(404);
110110
expect(r.json().error.code).toBe('not_found');
111111
});
112+
113+
// Agent crawlers fetch these from the API origin (not the docs
114+
// host); a 404 here loses the listing.
115+
test('GET /llms.txt serves the agent-facing service summary', async () => {
116+
const r = await call('GET', '/llms.txt');
117+
expect(r.statusCode).toBe(200);
118+
expect(r.headers['content-type']).toMatch(/text\/plain/);
119+
expect(r.body).toMatch(/Seneschal/);
120+
expect(r.body).toMatch(/openapi\.json/);
121+
});
122+
123+
test('GET /favicon.ico serves an icon', async () => {
124+
const r = await call('GET', '/favicon.ico');
125+
expect(r.statusCode).toBe(200);
126+
expect(r.headers['content-type']).toMatch(/image\/x-icon/);
127+
});
112128
});
113129

114130
describe('/v1/ops/health (watchdog state surface)', () => {

0 commit comments

Comments
 (0)