diff --git a/mcp-server/index.ts b/mcp-server/index.ts index 3b7f457..c1b2fcb 100644 --- a/mcp-server/index.ts +++ b/mcp-server/index.ts @@ -40,6 +40,16 @@ import { USDC_CONTRACT, AMOUNT_STROOPS, } from '../src/lib/constants' +import { formatReceipt } from './receipt' +import type { + SearchResponse, + ImageSearchResponse, + NewsSearchResponse, + ApiErrorResponse, + SearchResult, + ImageResult, + NewsResult, +} from '../src/types/index.js' import { formatConfigurationError, readMcpConfig } from '../src/lib/config' dotenv.config() @@ -230,6 +240,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ name: 'web_search', description: `Search the web via StellarSearch. Automatically pays ${AMOUNT_USDC} USDC on Stellar (x402 protocol). The server handles the full payment flow: HTTP 402 โ†’ sign Soroban auth โ†’ settle โ†’ return results. +Paid responses include a verifiable x402 receipt with on-chain transaction hash and Stellar Expert explorer link. Use for current events, documentation, research, or anything needing up-to-date web information.`, inputSchema: { type: 'object', @@ -245,6 +256,7 @@ Use for current events, documentation, research, or anything needing up-to-date name: 'image_search', description: `Search the web for images via StellarSearch. Automatically pays ${AMOUNT_USDC} USDC on Stellar (x402 protocol). Returns image URLs, titles, and source domains via the Serper.dev images API. +Paid responses include a verifiable x402 receipt with on-chain transaction hash and Stellar Expert explorer link. Use for visual references, photos, diagrams, or anything where you need image results.`, inputSchema: { type: 'object', @@ -259,6 +271,7 @@ Use for visual references, photos, diagrams, or anything where you need image re name: 'news_search', description: `Search recent news articles via StellarSearch. Automatically pays ${AMOUNT_USDC} USDC on Stellar (x402 protocol). Returns articles with title, URL, snippet, publication date, and source via the Serper.dev news API. +Paid responses include a verifiable x402 receipt with on-chain transaction hash and Stellar Expert explorer link. Use for breaking stories, current events, and time-sensitive reporting.`, inputSchema: { type: 'object', @@ -484,6 +497,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { throw new Error(e.error || `HTTP ${res.status}`) } + const data = await res.json() as any + const receipt = formatReceipt({ + txHash: data.txHash, + paidAmount: data.paidAmount, + currency: data.currency, + network: data.network, + x402Version: data.x402Version, + }) + const data = (await res.json()) as SearchResponse + const formatted = data.results + .map((r: SearchResult, i: number) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.description}`) const data = (await res.json()) as any // Record receipt (opted-in only, bounded) try { @@ -520,6 +544,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { cleanup() return { + content: [{ + type: 'text', + text: [ + `๐Ÿ” Results for: "${query}"`, + `๐Ÿ’ฐ Paid: ${data.paidAmount} ${data.currency} on ${data.network}`, + ...receipt.lines, + `โšก Latency: ${data.latencyMs}ms`, + `๐Ÿ“Š ${data.count} results\n`, + formatted, + ].join('\n'), + }], content: [ { type: 'text', @@ -563,6 +598,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { throw new Error(e.error || `HTTP ${res.status}`) } + const data: any = await res.json() + const receipt = formatReceipt({ + txHash: data.txHash, + paidAmount: data.paidAmount, + currency: data.currency, + network: data.network, + x402Version: data.x402Version, + }) + const data = (await res.json()) as ImageSearchResponse + const formatted = data.results + .map((r: ImageResult, i: number) => `${i + 1}. **${r.title}**\n Image: ${r.imageUrl}\n Source: ${r.sourceUrl} (${r.source})`) const data = (await res.json()) as any addMcpReceipt({ id: data.txHash || `${Date.now()}-img-${query.slice(0, 6)}`, @@ -581,6 +627,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { cleanup() return { + content: [{ + type: 'text', + text: [ + `๐Ÿ–ผ๏ธ Image results for: "${query}"`, + `๐Ÿ’ฐ Paid: ${data.paidAmount} ${data.currency} on ${data.network}`, + ...receipt.lines, + `โšก Latency: ${data.latencyMs}ms`, + `๐Ÿ“Š ${data.count} results\n`, + formatted, + ].join('\n'), + }], content: [ { type: 'text', @@ -631,6 +688,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { throw new Error(e.error || `HTTP ${res.status}`) } + const data: any = await res.json() + const receipt = formatReceipt({ + txHash: data.txHash, + paidAmount: data.paidAmount, + currency: data.currency, + network: data.network, + x402Version: data.x402Version, + }) + const data = (await res.json()) as NewsSearchResponse + const formatted = data.results + .map((r: NewsResult, i: number) => { const data = (await res.json()) as any addMcpReceipt({ id: data.txHash || `${Date.now()}-news-${query.slice(0, 6)}`, @@ -652,6 +720,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { cleanup() return { + content: [{ + type: 'text', + text: [ + `๐Ÿ“ฐ News results for: "${query}"`, + `๐Ÿ’ฐ Paid: ${data.paidAmount} ${data.currency} on ${data.network}`, + ...receipt.lines, + `โšก Latency: ${data.latencyMs}ms`, + `๐Ÿ“Š ${data.count} results\n`, + formatted, + ].join('\n'), + }], content: [ { type: 'text', diff --git a/mcp-server/receipt.test.ts b/mcp-server/receipt.test.ts new file mode 100644 index 0000000..55ba766 --- /dev/null +++ b/mcp-server/receipt.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect } from 'vitest' +import { formatReceipt, explorerTxUrl, PaymentReceiptData } from './receipt' +import { STELLAR_EXPERT_URL, STELLAR_NETWORK } from '../src/lib/constants' + +describe('mcp-server/receipt โ€” x402 receipt metadata', () => { + describe('explorerTxUrl', () => { + it('builds a Stellar Expert tx URL using the configured network', () => { + const hash = 'a1b2c3d4e5f678901234567890abcdef1234567890abcdef1234567890abcdef' + const url = explorerTxUrl(hash) + expect(url).toBe(`${STELLAR_EXPERT_URL}/tx/${hash}`) + }) + + it('contains /tx/ path and starts with https', () => { + const url = explorerTxUrl('deadbeef') + expect(url).toContain('/tx/deadbeef') + expect(url.startsWith('https://')).toBe(true) + }) + }) + + describe('formatReceipt โ€” verified receipt (txHash present)', () => { + const verifiedData: PaymentReceiptData = { + txHash: 'a1b2c3d4e5f678901234567890abcdef1234567890abcdef1234567890abcdef', + paidAmount: '0.001', + currency: 'USDC', + network: 'stellar:testnet', + x402Version: 2, + } + + it('returns verified: true when txHash is present', () => { + const result = formatReceipt(verifiedData) + expect(result.verified).toBe(true) + }) + + it('includes the full transaction hash in output', () => { + const result = formatReceipt(verifiedData) + const txLine = result.lines.find(l => l.startsWith(' Tx:')) + expect(txLine).toBe(` Tx: ${verifiedData.txHash}`) + }) + + it('includes the Stellar Expert explorer URL', () => { + const result = formatReceipt(verifiedData) + const explorerLine = result.lines.find(l => l.startsWith(' Explorer:')) + expect(explorerLine).toContain('stellar.expert') + expect(explorerLine).toContain(verifiedData.txHash!) + }) + + it('includes the x402 protocol version', () => { + const result = formatReceipt(verifiedData) + const receiptLine = result.lines.find(l => l.startsWith('๐Ÿ“„ Receipt:')) + expect(receiptLine).toContain('x402 v2') + }) + + it('includes the network', () => { + const result = formatReceipt(verifiedData) + const networkLine = result.lines.find(l => l.startsWith(' Network:')) + expect(networkLine).toBe(' Network: stellar:testnet') + }) + + it('defaults x402 version to 2 when not provided', () => { + const data: PaymentReceiptData = { + txHash: 'abc123', + network: 'stellar:testnet', + } + const result = formatReceipt(data) + const receiptLine = result.lines.find(l => l.startsWith('๐Ÿ“„ Receipt:')) + expect(receiptLine).toContain('x402 v2') + }) + + it('uses STELLAR_NETWORK when network field is omitted', () => { + const data: PaymentReceiptData = { txHash: 'abc123' } + const result = formatReceipt(data) + const networkLine = result.lines.find(l => l.startsWith(' Network:')) + expect(networkLine).toBe(` Network: ${STELLAR_NETWORK}`) + }) + }) + + describe('formatReceipt โ€” unverified receipt (missing txHash)', () => { + it('returns verified: false when txHash is null', () => { + const result = formatReceipt({ txHash: null }) + expect(result.verified).toBe(false) + }) + + it('returns verified: false when txHash is undefined', () => { + const result = formatReceipt({}) + expect(result.verified).toBe(false) + }) + + it('returns verified: false when txHash is empty string', () => { + const result = formatReceipt({ txHash: ' ' }) + expect(result.verified).toBe(false) + }) + + it('includes an explicit unverified warning', () => { + const result = formatReceipt({ txHash: null }) + const receiptLine = result.lines.find(l => l.startsWith('๐Ÿ“„ Receipt:')) + expect(receiptLine).toContain('โš ๏ธ') + expect(receiptLine).toContain('Unverified') + expect(receiptLine).toContain('no on-chain settlement proof returned') + }) + + it('shows amount and currency when available for unverified receipts', () => { + const data: PaymentReceiptData = { + txHash: null, + paidAmount: '0.001', + currency: 'USDC', + network: 'stellar:testnet', + } + const result = formatReceipt(data) + const amountLine = result.lines.find(l => l.startsWith(' Amount:')) + expect(amountLine).toBe(' Amount: 0.001 USDC') + }) + + it('omits amount line when paidAmount or currency is missing', () => { + const data: PaymentReceiptData = { txHash: null } + const result = formatReceipt(data) + const amountLine = result.lines.find(l => l.startsWith(' Amount:')) + expect(amountLine).toBeUndefined() + }) + + it('always includes at least one line', () => { + const result = formatReceipt({}) + expect(result.lines.length).toBeGreaterThanOrEqual(1) + }) + }) + + describe('formatReceipt โ€” edge cases', () => { + it('trims whitespace from txHash before deciding verification', () => { + const result = formatReceipt({ txHash: ' abc123 ' }) + expect(result.verified).toBe(true) + const txLine = result.lines.find(l => l.startsWith(' Tx:')) + expect(txLine).toBe(' Tx: abc123') + }) + + it('handles all fields provided with different x402 version', () => { + const data: PaymentReceiptData = { + txHash: 'ff00ff00', + paidAmount: '0.01', + currency: 'USDC', + network: 'stellar:mainnet', + x402Version: 1, + } + const result = formatReceipt(data) + expect(result.verified).toBe(true) + const receiptLine = result.lines.find(l => l.startsWith('๐Ÿ“„ Receipt:')) + expect(receiptLine).toContain('x402 v1') + const networkLine = result.lines.find(l => l.startsWith(' Network:')) + expect(networkLine).toBe(' Network: stellar:mainnet') + }) + }) +}) diff --git a/mcp-server/receipt.ts b/mcp-server/receipt.ts new file mode 100644 index 0000000..38fa66f --- /dev/null +++ b/mcp-server/receipt.ts @@ -0,0 +1,82 @@ +/** + * receipt.ts โ€” x402 receipt formatting for StellarSearch MCP tools. + * + * Surfaces verifiable payment receipt metadata (transaction hash, + * explorer URL, x402 protocol version) from server responses, and + * explicitly represents missing/unverified receipts. + */ + +import { STELLAR_EXPERT_URL, STELLAR_NETWORK } from '../src/lib/constants' + +/** Minimal shape of the receipt fields returned by the StellarSearch API. */ +export interface PaymentReceiptData { + /** On-chain transaction hash (hex string), if settlement completed. */ + txHash?: string | null + /** Amount paid per query. */ + paidAmount?: string | null + /** Currency code (e.g. "USDC"). */ + currency?: string | null + /** Network identifier (e.g. "stellar:testnet"). */ + network?: string | null + /** x402 protocol version used for settlement, if available. */ + x402Version?: number | null +} + +export interface ReceiptResult { + /** Whether the receipt has a verifiable on-chain transaction. */ + verified: boolean + /** Formatted receipt lines for MCP text output (always โ‰ฅ1 line). */ + lines: string[] +} + +/** + * Builds the Stellar Expert explorer URL for a given transaction hash. + * Uses the currently-configured network (testnet/mainnet) from constants. + */ +export function explorerTxUrl(txHash: string): string { + return `${STELLAR_EXPERT_URL}/tx/${txHash}` +} + +/** + * Formats x402 receipt metadata from a server response into human-readable + * MCP output lines. + * + * When `txHash` is present and non-empty, the receipt is marked as + * **Verified** with a clickable explorer URL. When the hash is missing, + * the receipt is explicitly marked as **โš ๏ธ Unverified** so consumers + * never confuse a missing settlement proof with a confirmed one. + * + * @param data - The payment-related fields from the server JSON response. + * @returns ReceiptResult containing a verified flag and formatted text lines. + */ +export function formatReceipt(data: PaymentReceiptData): ReceiptResult { + const network = data.network || STELLAR_NETWORK + const txHash = data.txHash?.trim() || null + const version = data.x402Version ?? 2 + + if (txHash) { + const url = explorerTxUrl(txHash) + return { + verified: true, + lines: [ + `๐Ÿ“„ Receipt: โœ… Verified (x402 v${version})`, + ` Tx: ${txHash}`, + ` Explorer: ${url}`, + ` Network: ${network}`, + ], + } + } + + // Explicitly represent the missing-receipt case rather than + // silently treating it as a successful paid response. + return { + verified: false, + lines: [ + `๐Ÿ“„ Receipt: โš ๏ธ Unverified โ€” no on-chain settlement proof returned`, + ` Network: ${network}`, + ...(data.paidAmount && data.currency + ? [` Amount: ${data.paidAmount} ${data.currency}`] + : []), + ], + } +} diff --git a/src/components/ai/GroqAssistant.tsx b/src/components/ai/GroqAssistant.tsx index 4779c96..7ca2ee8 100644 --- a/src/components/ai/GroqAssistant.tsx +++ b/src/components/ai/GroqAssistant.tsx @@ -259,11 +259,12 @@ export function GroqAssistant({ lastSearch }: Props = {}) {
- - @@ -339,7 +340,7 @@ export function GroqAssistant({ lastSearch }: Props = {}) { {/* Show model metadata for assistant messages */} {msg.role === 'assistant' && msg.model && ( -
+
{getModelLabel(msg.model)}
)} @@ -373,7 +374,8 @@ export function GroqAssistant({ lastSearch }: Props = {}) { onKeyDown={e => e.key === 'Enter' && !e.shiftKey && send()} placeholder="Ask anything..." disabled={loading} - className="flex-1 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-xs text-white placeholder:text-white/25 outline-none focus:border-neon-cyan/30 disabled:opacity-50" + aria-label="Ask the AI assistant" + className="flex-1 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-xs text-white placeholder:text-white/40 outline-none focus:border-neon-cyan/30 disabled:opacity-50" style={{ caretColor: '#00f5ff' }} />
+ + STELLARSEARCH ยท Stellar Hackathon 2026 {t('footer.tagline')} @@ -33,7 +35,7 @@ export function Footer() { href={href} target="_blank" rel="noopener noreferrer" - className="font-display text-xs text-white/20 hover:text-neon-cyan/60 transition-colors hidden sm:inline" + className="font-display text-xs text-white/55 hover:text-neon-cyan/80 transition-colors hidden sm:inline" > {label} diff --git a/src/components/layout/LiveTicker.tsx b/src/components/layout/LiveTicker.tsx index 4aacdf8..eb7e809 100644 --- a/src/components/layout/LiveTicker.tsx +++ b/src/components/layout/LiveTicker.tsx @@ -41,7 +41,7 @@ export function LiveTicker({ walletConnected }: Props) { {doubled.map(([k, v], i) => (
{k} @@ -52,7 +52,7 @@ export function LiveTicker({ walletConnected }: Props) { > {v} - โ—† +
))} diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 490cce6..a15fb9f 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -76,7 +76,7 @@ export function Navbar({ onClick={() => onNavigate(id)} aria-current={page === id ? "page" : undefined} className="relative flex items-center gap-1.5 px-3 py-1.5 rounded-lg font-display text-xs tracking-wider transition-colors" - style={{ color: page === id ? '#00f5ff' : 'rgba(255,255,255,0.3)' }} + style={{ color: page === id ? '#00f5ff' : 'rgba(255,255,255,0.5)' }} > {t(labelKey)} @@ -109,7 +109,7 @@ export function Navbar({ href="https://github.com/stellar/x402-stellar" target="_blank" rel="noopener noreferrer" - className="p-2 rounded-lg text-white/25 hover:text-white/55 hover:bg-white/5 transition-all" + className="p-2 rounded-lg text-white/45 hover:text-white/65 hover:bg-white/5 transition-all" > @@ -117,7 +117,7 @@ export function Navbar({ href="https://developers.stellar.org/docs/build/agentic-payments/x402" target="_blank" rel="noopener noreferrer" - className="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-lg font-display text-xs text-white/25 hover:text-white/50 transition-all" + className="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-lg font-display text-xs text-white/55 hover:text-white/75 transition-all" style={{ border: '1px solid rgba(255,255,255,0.08)' }} > x402 DOCS diff --git a/src/components/search/PaymentFlowVisualizer.tsx b/src/components/search/PaymentFlowVisualizer.tsx index 70c8972..559b4e2 100644 --- a/src/components/search/PaymentFlowVisualizer.tsx +++ b/src/components/search/PaymentFlowVisualizer.tsx @@ -47,7 +47,7 @@ export function PaymentFlowVisualizer({ session }: Props) { > {/* Header */}
- x402 PAYMENT FLOW + x402 PAYMENT FLOW {session.status === 'complete' && โœ“ SETTLED} {session.status === 'error' && โœ— FAILED}
@@ -108,7 +108,7 @@ export function PaymentFlowVisualizer({ session }: Props) { }}> {step.label}

-

{step.sub}

+

{step.sub}

) @@ -132,7 +132,7 @@ export function PaymentFlowVisualizer({ session }: Props) { transition={{ duration: 0.7, repeat: Infinity }} /> )} -

+

{isSearching && `โ†’ Step ${session.step ?? 1}/${TOTAL_STEPS}: ${STEPS[activeIdx]?.label} โ€” ${STEPS[activeIdx]?.sub}...`} {isComplete && `โœ“ Payment settled โ€” ${session.results.length} results in ${session.durationMs}ms`} {isError && `โœ— ${session.error}`} diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 16d9c4c..befe94f 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -115,7 +115,7 @@ export function SearchBar({ : 'Search anything โ€” pay per query, not per month...' } disabled={isSearching || isWrongNetwork} - className="flex-1 min-w-0 bg-transparent text-white placeholder:text-white/20 text-sm outline-none disabled:opacity-50" + className="flex-1 min-w-0 bg-transparent text-white placeholder:text-white/40 text-sm outline-none disabled:opacity-50" style={{ caretColor: isWrongNetwork ? '#ef4444' : '#00f5ff' }} /> @@ -130,10 +130,8 @@ export function SearchBar({ borderColor: isSearching || isWrongNetwork ? 'rgba(255,255,255,0.1)' - : 'rgba(0,245,255,0.4)', - color: - isSearching || isWrongNetwork - ? 'rgba(255,255,255,0.3)' + : 'rgba(0,245,255,0.4)', color: isSearching || isWrongNetwork + ? 'rgba(255,255,255,0.5)' : '#00f5ff', }} whileTap={{ scale: 0.96 }} @@ -160,7 +158,7 @@ export function SearchBar({ aria-label="Date range filters" > - Freshness: + Freshness: {FRESHNESS_OPTIONS.map((opt) => { const isSelected = freshness === opt.value @@ -177,7 +175,7 @@ export function SearchBar({ borderColor: isSelected ? 'rgba(0,245,255,0.5)' : 'rgba(255,255,255,0.08)', - color: isSelected ? '#00f5ff' : 'rgba(255,255,255,0.4)', + color: isSelected ? '#00f5ff' : 'rgba(255,255,255,0.55)', }} aria-pressed={isSelected} > @@ -189,14 +187,14 @@ export function SearchBar({ {/* Meta row */}

-

+

{walletConnected ? `Balance: ${usdcBalance} USDC ยท ~${Math.floor( parseFloat(usdcBalance) / parseFloat(AMOUNT_USDC) ).toLocaleString()} queries left` : 'Connect Freighter wallet to search'}

-

+

Serper.dev ยท x402 ยท Stellar {IS_MAINNET ? 'Mainnet' : 'Testnet'}

diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index 964a697..99a7bba 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -196,7 +196,7 @@ export function SearchResults({ results, query, isLoading, txHash }: Props) {
-

+

{results.length} RESULTS ยท SERPER.DEV ยท PAID VIA x402

{txHash && ( @@ -249,7 +249,7 @@ export function SearchResults({ results, query, isLoading, txHash }: Props) {
JSON Format
-
Structured data export
+
Structured data export
@@ -315,7 +315,7 @@ export function SearchResults({ results, query, isLoading, txHash }: Props) { {summaryError ? (

โš  {summaryError}

) : ( -

+

{summary} {summarizing && โ–Œ}

@@ -362,7 +362,7 @@ export function SearchResults({ results, query, isLoading, txHash }: Props) { {(r.relevanceScore * 100).toFixed(0)}%
{r.publishedAt && ( -
+
{r.publishedAt}
@@ -400,10 +400,10 @@ export function SearchResults({ results, query, isLoading, txHash }: Props) { {/* Copy button */}
-

+

{r.description}

-
+
diff --git a/src/components/search/SearchSuggestions.tsx b/src/components/search/SearchSuggestions.tsx index a4d59e3..3bdeaf9 100644 --- a/src/components/search/SearchSuggestions.tsx +++ b/src/components/search/SearchSuggestions.tsx @@ -36,7 +36,7 @@ export function SearchSuggestions({ onSelect, aiSuggestions }: Props) { >
{isAi && } -

+

{isAi ? 'YOU MIGHT ALSO SEARCH FOR' : 'TRY THESE'}

@@ -49,7 +49,7 @@ export function SearchSuggestions({ onSelect, aiSuggestions }: Props) { animate={{ opacity: 1, y: 0 }} transition={{ delay: i * 0.06 }} onClick={() => onSelect(q)} - className="px-3 py-1.5 rounded-lg text-xs font-display tracking-wide transition-all text-white/40 hover:text-neon-cyan/80" + className="px-3 py-1.5 rounded-lg text-xs font-display tracking-wide transition-all text-white/55 hover:text-neon-cyan/80" style={{ border: '1px solid rgba(255,255,255,0.08)' }} onMouseEnter={e => { const el = e.currentTarget as HTMLButtonElement diff --git a/src/components/ui/StatsGrid.tsx b/src/components/ui/StatsGrid.tsx index c5db55f..b5aea82 100644 --- a/src/components/ui/StatsGrid.tsx +++ b/src/components/ui/StatsGrid.tsx @@ -96,7 +96,7 @@ export function StatsGrid({ pollingIntervalMs = 10_000 }: StatsGridProps) {

{fmt(stats[key])}

-

{label}

@@ -107,7 +107,7 @@ export function StatsGrid({ pollingIntervalMs = 10_000 }: StatsGridProps) {
- + SERVER {stats.status === 'online' ? 'ONLINE' : 'OFFLINE โ€” run: npm run server'}
diff --git a/src/components/ui/ZeroBalanceBanner.tsx b/src/components/ui/ZeroBalanceBanner.tsx index 9f00ad4..6e2adfe 100644 --- a/src/components/ui/ZeroBalanceBanner.tsx +++ b/src/components/ui/ZeroBalanceBanner.tsx @@ -131,7 +131,7 @@ export function ZeroBalanceBanner({ Get free USDC

-

+

New to Stellar?{' '} diff --git a/src/components/wallet/WalletPanel.tsx b/src/components/wallet/WalletPanel.tsx index 5c9dd06..99aa5da 100644 --- a/src/components/wallet/WalletPanel.tsx +++ b/src/components/wallet/WalletPanel.tsx @@ -114,6 +114,8 @@ export function WalletPanel({ {t('panelLabel')}

+ + {wallet.network} {isWrongNetwork && '(EXPECTED ' + EXPECTED_WALLET_NETWORK + ')'} {wallet.network} {isWrongNetwork && t('expectedNetwork', { network: EXPECTED_WALLET_NETWORK })} @@ -130,7 +132,7 @@ export function WalletPanel({ > {wallet.publicKey} -
@@ -187,6 +193,7 @@ export function WalletPanel({ transition={{ duration: 0.8, repeat: Infinity, ease: 'linear' }} />
+ ) : transactions.length === 0 ? (

No transactions yet

) : transactions.length === 0 ? (

{t('noTransactions')}

) : ( @@ -197,7 +204,7 @@ export function WalletPanel({ className="flex items-center justify-between py-1.5 px-2 rounded bg-white/3 hover:bg-white/5 transition-colors" >
-

+

{tx.type.replace('_', ' ')}

@@ -205,12 +212,12 @@ export function WalletPanel({ href={explorerTxUrl(tx.hash)} target="_blank" rel="noopener noreferrer" - className="font-mono text-white/25 hover:text-neon-cyan transition-colors flex items-center gap-1" + className="font-mono text-white/50 hover:text-neon-cyan transition-colors flex items-center gap-1" style={{ fontSize: '10px' }} > {truncateHash(tx.hash, 6)} - + {formatTimeAgo(tx.timestamp)}
@@ -231,7 +238,7 @@ export function WalletPanel({ href="https://www.circle.com/en/usdc" target="_blank" rel="noopener noreferrer" - className="flex-1 py-2 rounded-lg border border-neon-amber/20 text-center font-display text-[10px] text-neon-amber/70 hover:bg-neon-amber/5 transition-colors uppercase tracking-widest" + className="flex-1 py-2 min-h-11 rounded-lg border border-neon-amber/20 text-center font-display text-[10px] text-neon-amber/85 hover:bg-neon-amber/5 transition-colors uppercase tracking-widest" > {t('buyUsdc')} @@ -240,7 +247,7 @@ export function WalletPanel({ href="https://laboratory.stellar.org/#account-creator?network=test" target="_blank" rel="noopener noreferrer" - className="flex-1 py-2 rounded-lg border border-neon-cyan/20 text-center font-display text-[10px] text-neon-cyan/70 hover:bg-neon-cyan/5 transition-colors uppercase tracking-widest" + className="flex-1 py-2 min-h-11 rounded-lg border border-neon-cyan/20 text-center font-display text-[10px] text-neon-cyan/85 hover:bg-neon-cyan/5 transition-colors uppercase tracking-widest" > {t('fundTestnet')} diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index 1cd054c..ccd1768 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -56,18 +56,19 @@ export function DashboardPage({ transactions, txLoading, publicKey, usdcBalance, {/* Header */}
- LIVE BLOCKCHAIN DATA + LIVE BLOCKCHAIN DATA

DASHBOARD

- {networkLabel} + {networkLabel}
@@ -84,7 +85,7 @@ export function DashboardPage({ transactions, txLoading, publicKey, usdcBalance, style={{ background: 'rgba(6,13,20,0.7)', border: '1px solid rgba(0,245,255,0.12)' }} > @@ -123,7 +124,7 @@ export function DashboardPage({ transactions, txLoading, publicKey, usdcBalance, className="rounded-2xl p-8 text-center" style={{ background: 'rgba(6,13,20,0.5)', border: '1px solid rgba(255,255,255,0.06)' }} > -

Connect your Freighter wallet to see live account data

+

Connect your Freighter wallet to see live account data

)} @@ -138,7 +139,7 @@ export function DashboardPage({ transactions, txLoading, publicKey, usdcBalance, >
- USDC SPENT OVER TIME + USDC SPENT OVER TIME
@@ -146,14 +147,14 @@ export function DashboardPage({ transactions, txLoading, publicKey, usdcBalance,
- LIVE TRANSACTION HISTORY - ยท FROM STELLAR HORIZON + LIVE TRANSACTION HISTORY + ยท FROM STELLAR HORIZON
EXPLORER @@ -221,8 +222,8 @@ export function DashboardPage({ transactions, txLoading, publicKey, usdcBalance, ) : transactions.length === 0 ? (
-

NO TRANSACTIONS YET

- {!publicKey &&

Connect your wallet to see your history

} +

NO TRANSACTIONS YET

+ {!publicKey &&

Connect your wallet to see your history

}
) : ( transactions.map((tx, i) => ( @@ -251,12 +252,12 @@ export function DashboardPage({ transactions, txLoading, publicKey, usdcBalance, href={explorerTxUrl(tx.hash)} target="_blank" rel="noopener noreferrer" - className="font-mono text-white/25 hover:text-neon-cyan transition-colors flex items-center gap-1" + className="font-mono text-white/50 hover:text-neon-cyan transition-colors flex items-center gap-1" style={{ fontSize: '10px' }} > {truncateHash(tx.hash, 6)} - {formatTimeAgo(tx.timestamp)} + {formatTimeAgo(tx.timestamp)}
@@ -280,9 +281,11 @@ export function DashboardPage({ transactions, txLoading, publicKey, usdcBalance,
- SEARCH AUDIT LOG - ยท PERSISTED LOCALLY + SEARCH AUDIT LOG + ยท PERSISTED LOCALLY
+
+ {receipts.length} RECEIPTS
{receipts.length > 0 && (

{receipt.amount} USDC

-

+

{receipt.network.split(':')[1]}

@@ -366,9 +369,9 @@ export function DashboardPage({ transactions, txLoading, publicKey, usdcBalance, >
-

{label}

+

{label}

{value}

-

{sub}

+

{sub}

))} diff --git a/src/pages/DocsPage.tsx b/src/pages/DocsPage.tsx index ca2c32e..0b1b858 100644 --- a/src/pages/DocsPage.tsx +++ b/src/pages/DocsPage.tsx @@ -63,6 +63,9 @@ export function DocsPage() { {/* Header */} + DOCUMENTATION +

HOW IT WORKS

+

{t('kicker', 'DOCUMENTATION')}

{t('title', 'HOW IT WORKS')}

@@ -80,7 +83,7 @@ export function DocsPage() { href={href} target="_blank" rel="noopener noreferrer" - className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg font-display text-xs tracking-wider text-white/40 hover:text-neon-cyan transition-all" + className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg font-display text-xs tracking-wider text-white/55 hover:text-neon-cyan transition-all" style={{ border: '1px solid rgba(255,255,255,0.08)' }} > {label} @@ -92,7 +95,7 @@ export function DocsPage() { {/* x402 payment flow */}

- THE x402 PROTOCOL + THE x402 PROTOCOL

Payment flow

@@ -118,10 +121,10 @@ export function DocsPage() {
- {step.num} + {step.num}

{step.title}

-

{step.desc}

+

{step.desc}

{step.code} @@ -137,7 +140,7 @@ export function DocsPage() { {/* Real stack */}
- REAL STACK โ€” NO MOCKS + REAL STACK โ€” NO MOCKS

Technology used

@@ -153,14 +156,14 @@ export function DocsPage() { background: 'rgba(6,13,20,0.5)', }} > - + {label} {value} @@ -186,7 +189,7 @@ export function DocsPage() {

STELLAR HACKATHON 2026 ยท AGENTS ON STELLAR

-

+

Built for the Agents on Stellar hackathon (March 30 โ€“ April 13, 2026). Addresses the explicit demand signal: pay-per-query web search instead of monthly subscriptions. Uses real x402 protocol, real Stellar testnet transactions, real search results, and real Groq AI โ€” zero mock data. diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index 8906f76..1a4db16 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -97,7 +97,7 @@ export function SearchPage({ wallet, onConnectWallet, session, search, reset }: GET -

+

Real web search for AI agents.{' '} {AMOUNT_USDC} USDC per query settled on Stellar via x402. Powered by Serper.dev +{' '}