Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ dist
coverage
.DS_Store

# Markdown files (except skills)
# Markdown files (except skills and application content)
*.md
!README.md
!.claude/skills/**/*.md
!packages/*/src/**/*.md
28 changes: 28 additions & 0 deletions packages/relay/src/agent-meta/api-catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"linkset": [
{
"anchor": "https://facilitator.402.md/",
"service-desc": [
{
"href": "https://facilitator.402.md/swagger/json",
"type": "application/json",
"title": "402md Facilitator OpenAPI spec"
}
],
"service-doc": [
{
"href": "https://facilitator.402.md/swagger",
"type": "text/html",
"title": "402md Facilitator API reference"
}
],
"status": [
{
"href": "https://facilitator.402.md/health",
"type": "application/json",
"title": "Facilitator health check"
}
]
}
]
}
14 changes: 14 additions & 0 deletions packages/relay/src/agent-meta/assets.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
declare module '*.md' {
const content: string
export default content
}

declare module '*.json' {
const content: string
export default content
}

declare module '*.xml' {
const content: string
export default content
}
32 changes: 32 additions & 0 deletions packages/relay/src/agent-meta/dashboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 402md Facilitator — Activity dashboard

A read-only view of live facilitator activity: supported chains, cross-chain routes, top resources and sellers, recent transactions, and cost comparisons.

Canonical URL: https://facilitator.402.md/dashboard

> **If you are an agent, do not scrape this page.** Every number on the dashboard comes from a structured JSON endpoint. Use those directly.

## Structured endpoints (preferred)

| Endpoint | Purpose |
| -------------------------------- | ----------------------------------------------------------------------- |
| `GET /discovery/resources` | Active x402 resources. Filter by `network`, sort by `uses` or `volume`. |
| `GET /discovery/stats` | Aggregate facilitator stats over a time window. |
| `GET /discovery/routes` | Cross-chain route activity and status matrix. |
| `GET /discovery/sellers` | Top sellers by volume or transaction count. |
| `GET /discovery/transactions` | Recent transactions with chain, route, and status. |
| `GET /discovery/cost-comparison` | Per-route gas cost comparison. |
| `GET /health` | Dependency health (DB, Redis, Temporal). |
| `GET /swagger/json` | Full OpenAPI spec. |

Every `discovery/*` endpoint accepts a `window` query parameter (`1h`, `24h`, `7d`, `30d`, `all`) and standard `limit`/`offset` pagination.

## What's on the dashboard

- **Architecture snapshot** — chains supported, active routes, protocols.
- **Per-chain breakdown** — transactions, volume, and health per chain.
- **Route matrix** — every source → destination pair, with volume and latency.
- **Top resources** — most-used x402 resources, rankable by uses or volume.
- **Top sellers** — ranked by USDC received.
- **Recent transactions** — latest settlements across all chains.
- **Cost comparison** — gas allowance per route vs. competing rails.
79 changes: 79 additions & 0 deletions packages/relay/src/agent-meta/landing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 402md Facilitator

**Cross-chain USDC settlement for x402 and MPP.** Buyer pays on any supported chain, seller receives native USDC on their chain. One HTTP request to onboard. No SDK, no dashboard, no custom code.

Canonical URL: https://facilitator.402.md/

## Supported chains

All via Circle CCTP V2 with native USDC (no wrapped tokens, no slippage).

- **EVM**: Base, Ethereum, Optimism, Arbitrum, Linea, Unichain, World Chain
- **Solana**: mainnet
- **Stellar**: pubnet

## Getting started (seller)

```http
POST https://facilitator.402.md/register
Content-Type: application/json

{ "wallet": "GSELLER...", "network": "stellar:pubnet" }
```

The response returns your `merchantId` and facilitator pay-to addresses on every supported chain. Drop them into standard `@x402/express` and you are done.

## How it works

1. Seller registers their wallet once. No login, no dashboard.
2. Seller uses standard `@x402/express` middleware. Facilitator address as `payTo`, `merchantId` in `extra`. Zero 402md dependencies.
3. Buyer pays in USDC on any supported chain. Same-chain = direct transfer. Cross-chain = Circle CCTP V2 burns on source, mints native USDC on seller's chain.

Settlement latency:

- Same-chain: under 5 seconds
- Solana/Stellar cross-chain: 5-30 seconds
- EVM cross-chain: 15-19 minutes (block finality)

Buyer always gets the resource instantly — settlement runs in the background.

## Design choices

- **Non-custodial.** CCTP V2 mints directly to the seller's wallet. 402md never holds funds beyond the gas allowance needed to execute settlement.
- **Free.** 0% platform fee at launch. Only a fixed gas allowance is deducted per route.
- **Zero dependencies.** Sellers use the standard `@x402/express` SDK from Coinbase. The only 402md-specific step is `POST /register`.
- **Dual protocol.** Same facilitator serves x402 and MPP. Two ecosystems, one registration.
- **Native USDC only.** Circle CCTP V2 burns and mints real USDC. No DEX, no bridge tokens.

## Pricing

Zero platform fee. Only fixed gas allowance per route. On a $1.00 payment:

| Route | Seller receives | Gas allowance |
| ----------------- | --------------- | ----------------- |
| Stellar → Stellar | $0.99999 | $0.00001 (0.001%) |
| Solana → Solana | $0.9990 | $0.0010 (0.10%) |
| Base → Base | $0.9980 | $0.0020 (0.20%) |
| Stellar → Solana | $0.9985 | $0.0015 (0.15%) |
| Solana → Stellar | $0.9975 | $0.0025 (0.25%) |
| Stellar → Base | $0.9975 | $0.0025 (0.25%) |
| Base → Stellar | $0.9968 | $0.0032 (0.32%) |
| Base → Solana | $0.9965 | $0.0035 (0.35%) |
| Solana → Base | $0.9965 | $0.0035 (0.35%) |

## Discovery for agents

- `GET /health` — dependency health
- `GET /swagger` — OpenAPI docs (HTML)
- `GET /swagger/json` — OpenAPI spec (JSON)
- `GET /discovery/resources` — public registry of x402 resources
- `GET /discovery/routes` — cross-chain route and cost matrix
- `GET /.well-known/api-catalog` — RFC 9727 link set
- `GET /.well-known/agent-skills/index.json` — skills index for agent onboarding

## Ecosystem

- [x402 spec](https://www.x402.org/) — Coinbase's HTTP 402 protocol
- [MPP spec](https://github.com/AgoraPay-Stellar/mpp-specification) — Machine Payments Protocol
- [Circle CCTP V2](https://www.circle.com/cctp) — native USDC cross-chain
- [Source code](https://github.com/402md)
84 changes: 84 additions & 0 deletions packages/relay/src/agent-meta/negotiation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, test, expect } from 'bun:test'
import '@/shared/test-helpers'
import { Elysia } from 'elysia'
import { html } from '@elysiajs/html'
import { LandingPage } from '@/views/landing/page'
import { DashboardPage } from '@/views/dashboard/page'
import {
AGENT_DISCOVERY_LINK_HEADER,
landingMarkdown,
dashboardMarkdown,
wantsMarkdown,
} from './routes'

// Mirrors the `/` and `/dashboard` handlers in src/index.ts. Kept in sync
// so agent-facing behavior (Accept negotiation + Link header) has a test
// without spinning up the full relay (DB/Redis/Temporal) in unit tests.
const app = new Elysia()
.use(html())
.get('/', ({ request, set }) => {
set.headers['link'] = AGENT_DISCOVERY_LINK_HEADER
set.headers['vary'] = 'Accept'
if (wantsMarkdown(request.headers.get('accept'))) {
set.headers['content-type'] = 'text/markdown; charset=utf-8'
return landingMarkdown
}
return LandingPage()
})
.get('/dashboard', ({ request, set }) => {
set.headers['link'] = AGENT_DISCOVERY_LINK_HEADER
set.headers['vary'] = 'Accept'
if (wantsMarkdown(request.headers.get('accept'))) {
set.headers['content-type'] = 'text/markdown; charset=utf-8'
return dashboardMarkdown
}
return DashboardPage()
})

function get(path: string, headers: Record<string, string> = {}) {
return new Request(`http://localhost${path}`, { headers })
}

describe('GET / — content negotiation', () => {
test('returns HTML by default with Link and Vary headers', async () => {
const res = await app.handle(get('/'))
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('text/html')
expect(res.headers.get('vary')).toBe('Accept')
const link = res.headers.get('link')
expect(link).toContain('rel="api-catalog"')
expect(link).toContain('rel="service-desc"')
const body = await res.text()
expect(body.startsWith('<!doctype html>')).toBe(true)
})

test('returns markdown when Accept: text/markdown is preferred', async () => {
const res = await app.handle(get('/', { accept: 'text/markdown' }))
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('text/markdown')
expect(res.headers.get('vary')).toBe('Accept')
expect(res.headers.get('link')).toContain('rel="api-catalog"')
const body = await res.text()
expect(body).toContain('# 402md Facilitator')
expect(body).toContain('POST https://facilitator.402.md/register')
})
})

describe('GET /dashboard — content negotiation', () => {
test('returns HTML by default with Link and Vary headers', async () => {
const res = await app.handle(get('/dashboard'))
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('text/html')
expect(res.headers.get('vary')).toBe('Accept')
expect(res.headers.get('link')).toContain('rel="service-desc"')
})

test('returns markdown when Accept: text/markdown is preferred', async () => {
const res = await app.handle(get('/dashboard', { accept: 'text/markdown' }))
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('text/markdown')
const body = await res.text()
expect(body).toContain('# 402md Facilitator — Activity dashboard')
expect(body).toContain('/discovery/')
})
})
67 changes: 67 additions & 0 deletions packages/relay/src/agent-meta/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# robots.txt for facilitator.402.md
# 402md Facilitator is a public HTTP API designed for AI agents.
# Crawling, indexing, and real-time agent retrieval are welcome.
# The live dashboard is excluded from indexing because it is already
# exposed as a structured API under /discovery/*.

User-agent: *
# Content Signals (https://contentsignals.org/)
# ai-train=no : do not use this content for training
# search=yes : index for search, including AI-assisted search
# ai-input=yes: agents may read this content at query time
Content-Signal: search=yes, ai-train=no, ai-input=yes
Allow: /
Disallow: /dashboard

# ── AI / LLM crawlers ─────────────────────────────────────────────
# Training crawlers: disallowed. This site's content is marketing
# and API reference, not a training corpus.
# Real-time retrieval agents: allowed. Helping agents use the
# facilitator is literally the product.

User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: Google-Extended
Disallow: /

User-agent: CCBot
Disallow: /

User-agent: anthropic-ai
Disallow: /

User-agent: Meta-ExternalAgent
Disallow: /

User-agent: Applebot-Extended
Disallow: /

User-agent: OAI-SearchBot
Allow: /
Disallow: /dashboard

User-agent: ChatGPT-User
Allow: /
Disallow: /dashboard

User-agent: Claude-Web
Allow: /
Disallow: /dashboard

User-agent: Claude-User
Allow: /
Disallow: /dashboard

User-agent: PerplexityBot
Allow: /
Disallow: /dashboard

User-agent: Perplexity-User
Allow: /
Disallow: /dashboard

Sitemap: https://facilitator.402.md/sitemap.xml
Loading
Loading