diff --git a/packages/relay/src/views/dashboard/client-script.ts b/packages/relay/src/views/dashboard/client-script.ts index 97f26c8..4eb72ef 100644 --- a/packages/relay/src/views/dashboard/client-script.ts +++ b/packages/relay/src/views/dashboard/client-script.ts @@ -124,11 +124,11 @@ export const dashboardClientScript = /* js */ ` } let html = '' - for (const c of CHAINS) html += '' + for (const c of CHAINS) html += '' html += '' for (const row of CHAINS) { - html += '' + html += '' for (const col of CHAINS) { const r = lookup.get(row.caip2 + '|' + col.caip2) const sameChain = row.caip2 === col.caip2 diff --git a/packages/relay/src/views/dashboard/components.tsx b/packages/relay/src/views/dashboard/components.tsx index fb81792..0c9487a 100644 --- a/packages/relay/src/views/dashboard/components.tsx +++ b/packages/relay/src/views/dashboard/components.tsx @@ -87,7 +87,7 @@ interface LoadingSlotProps { // Reserved container that the client script hydrates on first poll. export const LoadingSlot = ({ id, message = 'Loading…' }: LoadingSlotProps) => ( -
+

{message}

) diff --git a/packages/relay/src/views/dashboard/config.ts b/packages/relay/src/views/dashboard/config.ts index 2668334..4f7448d 100644 --- a/packages/relay/src/views/dashboard/config.ts +++ b/packages/relay/src/views/dashboard/config.ts @@ -2,20 +2,21 @@ export interface ChainConfig { slug: string caip2: string label: string + symbol: string } // Ordered alphabetically on purpose — the per-chain grid renders with equal // visual weight regardless of volume. export const CHAINS: ChainConfig[] = [ - { slug: 'arbitrum', caip2: 'eip155:42161', label: 'Arbitrum' }, - { slug: 'base', caip2: 'eip155:8453', label: 'Base' }, - { slug: 'ethereum', caip2: 'eip155:1', label: 'Ethereum' }, - { slug: 'linea', caip2: 'eip155:59144', label: 'Linea' }, - { slug: 'optimism', caip2: 'eip155:10', label: 'Optimism' }, - { slug: 'solana', caip2: 'solana:mainnet', label: 'Solana' }, - { slug: 'stellar', caip2: 'stellar:pubnet', label: 'Stellar' }, - { slug: 'unichain', caip2: 'eip155:130', label: 'Unichain' }, - { slug: 'worldchain', caip2: 'eip155:480', label: 'World Chain' }, + { slug: 'arbitrum', caip2: 'eip155:42161', label: 'Arbitrum', symbol: 'ARB' }, + { slug: 'base', caip2: 'eip155:8453', label: 'Base', symbol: 'BASE' }, + { slug: 'ethereum', caip2: 'eip155:1', label: 'Ethereum', symbol: 'ETH' }, + { slug: 'linea', caip2: 'eip155:59144', label: 'Linea', symbol: 'LIN' }, + { slug: 'optimism', caip2: 'eip155:10', label: 'Optimism', symbol: 'OP' }, + { slug: 'solana', caip2: 'solana:mainnet', label: 'Solana', symbol: 'SOL' }, + { slug: 'stellar', caip2: 'stellar:pubnet', label: 'Stellar', symbol: 'XLM' }, + { slug: 'unichain', caip2: 'eip155:130', label: 'Unichain', symbol: 'UNI' }, + { slug: 'worldchain', caip2: 'eip155:480', label: 'World Chain', symbol: 'WLD' }, ] export const EXPLORERS: Record = { diff --git a/packages/relay/src/views/dashboard/page.tsx b/packages/relay/src/views/dashboard/page.tsx index 74c9042..8d7711a 100644 --- a/packages/relay/src/views/dashboard/page.tsx +++ b/packages/relay/src/views/dashboard/page.tsx @@ -1,5 +1,6 @@ import { Layout } from '../layout' import { Nav } from '../nav' +import { TestnetBanner } from '../testnet-banner' import { dashboardStyles } from './styles' import { dashboardClientScript } from './client-script' import { CHAINS, EXPLORERS } from './config' @@ -15,6 +16,7 @@ export const DashboardPage = () => ( description="Live activity for the 402md facilitator: chains, cross-chain routes, resources, sellers, transactions." extraStyles={dashboardStyles} > +
- -
-
- -
-
- - terminal -
-
# 1. Register your wallet (one-time)
-curl -X POST https://facilitator.402.md/register \
-  -H "Content-Type: application/json" \
-  -d '{"wallet": "GSELLER...", "network": "stellar:pubnet"}'
-
-# 2. Use standard @x402/express — zero 402md code
-import { paymentMiddleware } from "@x402/express"
-
-app.use(paymentMiddleware({
-  "GET /your-endpoint": {
-    accepts: [
-      { scheme: "exact", network: "eip155:8453",    payTo: "0x4C95...c3b9", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Base
-      { scheme: "exact", network: "eip155:1",       payTo: "0x8aF2...71d4", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Ethereum
-      { scheme: "exact", network: "eip155:10",      payTo: "0x2E19...a0b7", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Optimism
-      { scheme: "exact", network: "eip155:42161",   payTo: "0xF1c3...9e82", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Arbitrum
-      { scheme: "exact", network: "eip155:59144",   payTo: "0x7b45...d60a", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Linea
-      { scheme: "exact", network: "eip155:130",     payTo: "0xD309...4cf1", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Unichain
-      { scheme: "exact", network: "eip155:480",     payTo: "0x5a6e...bb28", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // World Chain
-      { scheme: "exact", network: "solana:mainnet", payTo: "4r7q...BDP5", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Solana
-      { scheme: "exact", network: "stellar:pubnet", payTo: "GAX5...5HZR", price: "$0.001", extra: { merchantId: "7c-vQZHANEd5ICp" } }, // Stellar
-    ],
-  },
-}, "https://facilitator.402.md"))
-
-# Buyers on any of the 9 supported chains can now pay you.
-
-
-
-
@@ -119,19 +78,21 @@

Three steps. That's it.

Register your wallet

- POST /register with your address and chain. Get - facilitator addresses on all supported chains. + POST /register with your destination wallet. Get back a + facilitator address on every supported chain, plus a + merchantId that ties them all back to you.

02
-

Use standard x402

+

Paste into your x402 config

- Add @x402/express middleware. Point - payTo to your facilitator address. Zero 402md - dependencies. + Add one accepts entry per chain — each with its + facilitator payTo and your + merchantId. Works with any x402 server library; no 402md + SDK.

@@ -140,8 +101,8 @@

Use standard x402

Receive USDC on your chain

- Buyer pays on any chain. 402md settles via CCTP V2. Native USDC, directly to your - wallet. + Buyer pays on whichever chain they hold USDC on. 402md settles via CCTP V2 — + native USDC, minted directly into your registered wallet.

@@ -189,6 +150,161 @@

Get the resource

+ +
+
+ +

One curl. All chains.

+

+ Register your wallet and start accepting cross-chain USDC in under a minute. No dashboard, + no login, no SDK. +

+ +
+
+
+ +
+ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

0x followed by 40 hex characters

+ +
+ + API docs ↗ +
+
+ + + +
+
+
+
+
@@ -348,7 +464,7 @@

Free. Seriously.

- +
@@ -436,168 +552,6 @@

Common questions

- - -
-
- -

One curl. All chains.

-

- Register your wallet and start accepting cross-chain USDC in under a minute. No dashboard, - no login, no SDK. -

- -
- USDC - Base - Solana - Stellar -
- -
-
-
- -
- - - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -

0x followed by 40 hex characters

- -
- - API docs ↗ -
-
- - - -
-
-
-
diff --git a/packages/relay/src/views/landing/page.tsx b/packages/relay/src/views/landing/page.tsx index d0c4432..2862ebf 100644 --- a/packages/relay/src/views/landing/page.tsx +++ b/packages/relay/src/views/landing/page.tsx @@ -1,6 +1,7 @@ import { resolveNetworkEnv } from '@402md/shared/networks' import { Layout } from '../layout' import { Nav } from '../nav' +import { TestnetBanner } from '../testnet-banner' import landingContent from './content.html' with { type: 'text' } import landingStyles from './styles.css' with { type: 'text' } import landingScript from './script.txt' with { type: 'text' } @@ -24,6 +25,7 @@ export const LandingPage = () => ( description="Accept USDC payments from any chain. Buyer pays on Solana, seller receives on Stellar. One HTTP request, zero custom code, 0% fee." extraStyles={landingStyles} > + ) diff --git a/packages/relay/src/views/shared-styles.ts b/packages/relay/src/views/shared-styles.ts index c9d82ca..1649ae8 100644 --- a/packages/relay/src/views/shared-styles.ts +++ b/packages/relay/src/views/shared-styles.ts @@ -98,6 +98,33 @@ export const sharedStyles = /* css */ ` padding: 0 clamp(1rem, 5vw, 2.5rem); } + /* ══════════════════════════════════════════ + TESTNET BANNER — solid amber strip pinned above the nav + ══════════════════════════════════════════ */ + .testnet-banner { + background: var(--warn); + color: var(--text); + font-size: 0.75rem; + font-weight: 500; + text-align: center; + padding: var(--space-sm) var(--space-md); + } + .testnet-banner-cta { + color: inherit; + font-weight: 600; + text-decoration: underline; + text-underline-offset: 3px; + white-space: nowrap; + } + .testnet-banner-cta:hover { + text-decoration: none; + } + @media (min-width: 640px) { + .testnet-banner { + font-size: 0.875rem; + } + } + /* ══════════════════════════════════════════ NAV — canonical source, used by every page ══════════════════════════════════════════ */ @@ -161,78 +188,25 @@ export const sharedStyles = /* css */ ` font-weight: 500; } - .nav-cta { - display: inline-flex; - align-items: center; - margin-left: auto; - padding: var(--space-sm) var(--space-md); - background: var(--accent); - color: var(--bg); - border-radius: var(--radius); - font-size: 0.875rem; - font-weight: 500; - transition: - background-color 150ms, - transform 80ms; - } - .nav-cta:hover { - background: var(--accent-deep); - text-decoration: none; - } - .nav-cta:active { - transform: scale(0.97); - } - @media (min-width: 768px) { .nav-links { gap: var(--space-lg); font-size: 0.875rem; } - .nav-cta { - margin-left: 0; - } } - @media (max-width: 480px) { - .nav { - height: auto; - } + @media (max-width: 767px) { .nav-inner { - flex-wrap: wrap; - padding-top: 10px; - padding-bottom: 6px; - column-gap: var(--space-sm); - row-gap: var(--space-xs); - } - .nav-cta { - margin-left: auto; - padding: 6px 12px; - font-size: 0.8125rem; + gap: var(--space-md); } .nav-links { - order: 3; - width: 100%; - margin: 0; - gap: var(--space-lg); + gap: var(--space-md); font-size: 0.75rem; + min-width: 0; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; scroll-snap-type: x proximity; - -webkit-mask-image: linear-gradient( - to right, - transparent, - black 12px, - black calc(100% - 12px), - transparent - ); - mask-image: linear-gradient( - to right, - transparent, - black 12px, - black calc(100% - 12px), - transparent - ); } .nav-links::-webkit-scrollbar { display: none; @@ -244,20 +218,12 @@ export const sharedStyles = /* css */ ` } } - /* Very narrow phones — tighten CTA further so it doesn't crowd the brand. */ - @media (max-width: 360px) { - .nav-cta { - padding: 6px 10px; - } - } - /* ══════════════════════════════════════════ TOUCH TARGETS — enforce 44px on coarse pointers Matches Apple HIG / Material. Desktop mouse users keep the denser default. ══════════════════════════════════════════ */ @media (pointer: coarse) { - .nav-cta, .nav-links a, .btn-primary, .btn-outline, diff --git a/packages/relay/src/views/testnet-banner.tsx b/packages/relay/src/views/testnet-banner.tsx new file mode 100644 index 0000000..3c338cf --- /dev/null +++ b/packages/relay/src/views/testnet-banner.tsx @@ -0,0 +1,8 @@ +export const TestnetBanner = () => ( +
+ Running on testnet only —{' '} + + help us launch on mainnet → + +
+)
' + c.label + '' + c.symbol + '
' + row.label + '
' + row.symbol + '