Skip to content
Open
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
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,24 @@
# x402-directory
A curated, agent-maintained directory of x402 payment-enabled applications and endpoints. Browse, search, and discover services that accept x402 micropayments. Agents can contribute by adding new listings, verifying endpoint liveness, and categorizing services.
# x402 Directory

A curated, agent-maintained directory of x402 payment-enabled applications and endpoints.

## Listing detail page

This implementation adds a static listing detail experience with:

- Listing name and full description
- Endpoint URL
- Category
- Pricing
- Verification status
- Last checked date
- Maintainer metadata
- Link to try the endpoint
- Direct links with `?listing=<slug>`
- A custom directory thumbnail at `public/thumbnail.svg`

## Local validation

```bash
npm test
```
17 changes: 17 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="x402 Directory listing detail pages with endpoint URLs, pricing, verification status, and try links."
/>
<title>x402 Directory</title>
<link rel="stylesheet" href="./src/styles.css" />
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.js"></script>
</body>
</html>
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "x402-directory",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"test": "node --test tests/*.test.mjs"
}
}
14 changes: 14 additions & 0 deletions public/thumbnail.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions src/listing-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export function getListingBySlug(listings, slug) {
return listings.find((listing) => listing.slug === slug) ?? null;
}

export function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
}

export function formatPricing(listing) {
const { amount, currency, unit } = listing.pricing;
return `${amount.toFixed(2)} ${currency} / ${unit}`;
}

export function getVerificationLabel(listing) {
switch (listing.verificationStatus) {
case 'verified':
return 'Verified x402 endpoint';
case 'failed':
return 'Needs recheck';
default:
return 'Verification pending';
}
}

export function getVerificationClass(listing) {
switch (listing.verificationStatus) {
case 'verified':
return 'status-pill status-pill--verified';
case 'failed':
return 'status-pill status-pill--failed';
default:
return 'status-pill status-pill--pending';
}
}
41 changes: 41 additions & 0 deletions src/listings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const listings = [
{
slug: 'weather-meter',
name: 'Weather Meter',
url: 'https://weather.example/x402',
tryUrl: 'https://weather.example/demo',
description:
'A small x402 endpoint for current weather summaries by city. Useful for testing wallet-gated API calls and pay-per-request UX.',
category: 'Data APIs',
pricing: { amount: 0.02, currency: 'USDC', unit: 'request' },
verificationStatus: 'verified',
lastChecked: '2026-05-28',
maintainer: 'Weather Meter Labs'
},
{
slug: 'receipt-lens',
name: 'Receipt Lens',
url: 'https://receiptlens.example/pay',
tryUrl: 'https://receiptlens.example',
description:
'Transforms uploaded receipt images into structured JSON after x402 payment. Listed here as a reference flow for AI-agent commerce demos.',
category: 'AI tools',
pricing: { amount: 0.15, currency: 'USDC', unit: 'image' },
verificationStatus: 'pending',
lastChecked: '2026-05-26',
maintainer: 'Receipt Lens'
},
{
slug: 'dataset-postage',
name: 'Dataset Postage',
url: 'https://datasetpostage.example/x402/download',
tryUrl: 'https://datasetpostage.example/catalog',
description:
'A pay-per-download endpoint for small public-domain CSV bundles. Good for testing listing detail pages with richer pricing and verification metadata.',
category: 'Datasets',
pricing: { amount: 0.50, currency: 'USDC', unit: 'download' },
verificationStatus: 'verified',
lastChecked: '2026-05-29',
maintainer: 'Open Data Desk'
}
];
102 changes: 102 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
escapeHtml,
formatPricing,
getListingBySlug,
getVerificationClass,
getVerificationLabel
} from './listing-utils.js';
import { listings } from './listings.js';

const defaultListing = listings[0];
const params = new URLSearchParams(window.location.search);
const initialSlug = params.get('listing') ?? window.location.hash.replace('#', '') ?? defaultListing.slug;

function renderListingTabs(selectedSlug) {
return listings
.map((listing) => {
const activeClass = listing.slug === selectedSlug ? ' listing-tab--active' : '';
return `
<button class="listing-tab${activeClass}" type="button" data-listing-slug="${escapeHtml(listing.slug)}">
<span>${escapeHtml(listing.name)}</span>
<small>${escapeHtml(listing.category)}</small>
</button>
`;
})
.join('');
}

function renderListingDetail(slug) {
const listing = getListingBySlug(listings, slug) ?? defaultListing;
const app = document.querySelector('#app');

app.innerHTML = `
<header class="hero">
<img class="thumbnail" src="./public/thumbnail.svg" alt="x402 Directory thumbnail" />
<div>
<p class="eyebrow">x402 Directory</p>
<h1>Endpoint detail pages for pay-per-request services.</h1>
<p class="hero-copy">
Each listing exposes the endpoint URL, category, pricing, verification state, last checked date,
and a direct link to try the service.
</p>
</div>
</header>

<nav class="listing-tabs" aria-label="x402 listings">
${renderListingTabs(listing.slug)}
</nav>

<main class="detail-grid">
<section class="detail-card">
<div class="detail-heading">
<div>
<p class="eyebrow">${escapeHtml(listing.category)}</p>
<h2>${escapeHtml(listing.name)}</h2>
<p>${escapeHtml(listing.description)}</p>
</div>
<span class="${getVerificationClass(listing)}">${escapeHtml(getVerificationLabel(listing))}</span>
</div>

<dl class="metadata-grid">
<div>
<dt>Endpoint URL</dt>
<dd><a href="${escapeHtml(listing.url)}">${escapeHtml(listing.url)}</a></dd>
</div>
<div>
<dt>Pricing</dt>
<dd>${escapeHtml(formatPricing(listing))}</dd>
</div>
<div>
<dt>Last checked</dt>
<dd><time datetime="${escapeHtml(listing.lastChecked)}">${escapeHtml(listing.lastChecked)}</time></dd>
</div>
<div>
<dt>Maintainer</dt>
<dd>${escapeHtml(listing.maintainer)}</dd>
</div>
</dl>

<a class="try-link" href="${escapeHtml(listing.tryUrl)}">Try this endpoint</a>
</section>

<aside class="summary-card">
<h3>Detail page coverage</h3>
<ul>
<li>Name and description</li>
<li>URL and try link</li>
<li>Category and pricing</li>
<li>Verification status</li>
<li>Last checked date</li>
</ul>
</aside>
</main>
`;

window.history.replaceState({}, '', `?listing=${encodeURIComponent(listing.slug)}`);

document.querySelectorAll('[data-listing-slug]').forEach((button) => {
button.addEventListener('click', () => renderListingDetail(button.dataset.listingSlug));
});
}

renderListingDetail(initialSlug);
Loading