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
3 changes: 3 additions & 0 deletions templates/chat-x402/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ECHO_APP_ID=your-echo-app-id
NEXT_PUBLIC_ECHO_APP_ID=your-echo-app-id
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your-walletconnect-project-id
50 changes: 50 additions & 0 deletions templates/chat-x402/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Chat x402 Template

A simple AI chat template with an auth switcher that lets users choose between paying with **Echo credits** or **USDC via x402** protocol.

Based on the [next-chat](../next-chat) template with the wallet/x402 payment flow from the [sora-template](https://github.com/Merit-Systems/sora-template).

## Features

- Simple chat interface using Vercel AI SDK
- Auth switcher modal: Echo credits **or** wallet-based USDC (x402)
- RainbowKit wallet connection (Base, Mainnet)
- Echo SDK for authentication and billing
- Model selection (GPT-4o, GPT-5)

## Getting Started

1. Copy `.env.example` to `.env.local` and fill in your keys:

```bash
cp .env.example .env.local
```

2. Install dependencies:

```bash
npm install
```

3. Run dev server:

```bash
npm run dev
```

## Environment Variables

| Variable | Description |
| --- | --- |
| `ECHO_APP_ID` | Your Echo app ID (server-side) |
| `NEXT_PUBLIC_ECHO_APP_ID` | Your Echo app ID (client-side) |
| `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` | WalletConnect project ID for wallet connections |

## How It Works

Users are presented with a login modal offering two payment methods:

1. **Echo Credits** — Sign in via Echo OAuth, pay per-message with Echo credits
2. **USDC via x402** — Connect a wallet (e.g. MetaMask), pay with USDC using the [x402](https://www.x402.org/) payment protocol

Once authenticated through either method, users get access to the same chat interface.
22 changes: 22 additions & 0 deletions templates/chat-x402/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
19 changes: 19 additions & 0 deletions templates/chat-x402/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FlatCompat } from '@eslint/eslintrc'

const compat = new FlatCompat({
// import.meta.dirname is available after Node.js v20.11.0
baseDirectory: import.meta.dirname,
})

const eslintConfig = [
...compat.config({
extends: ['next'],
settings: {
next: {
rootDir: 'examples/nextjs-chatbot/',
},
},
}),
]

export default eslintConfig
10 changes: 10 additions & 0 deletions templates/chat-x402/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
/* config options here */
turbopack: {
root: '.',
},
};

export default nextConfig;
73 changes: 73 additions & 0 deletions templates/chat-x402/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"name": "chat-x402-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "biome check --write",
"postinstall": "npm dedupe || true"
},
"dependencies": {
"@ai-sdk/react": "2.0.17",
"@merit-systems/echo-next-sdk": "latest",
"@merit-systems/echo-react-sdk": "latest",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@rainbow-me/rainbowkit": "^2.2.8",
"@tanstack/react-query": "^5.90.2",
"ai": "5.0.19",
"@ai-sdk/openai": "2.0.16",
"autonumeric": "^4.10.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.542.0",
"next": "15.5.9",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-syntax-highlighter": "^15.6.6",
"shiki": "^3.12.2",
"streamdown": "^1.2.0",
"tailwind-merge": "^3.3.1",
"use-stick-to-bottom": "^1.1.1",
"viem": "2.x",
"wagmi": "^2.17.5",
"x402": "^0.6.6",
"zod": "^4.1.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@next/eslint-plugin-next": "^15.5.3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
"tailwindcss": "^4",
"tw-animate-css": "^1.3.8",
"typescript": "^5"
},
"overrides": {
"ai": "5.0.19",
"@ai-sdk/react": "2.0.17",
"@ai-sdk/openai": "2.0.16",
"@merit-systems/echo-react-sdk": {
"ai": "5.0.19",
"@ai-sdk/react": "2.0.17"
},
"@merit-systems/echo-next-sdk": {
"ai": "5.0.19",
"@ai-sdk/openai": "2.0.16"
}
}
}
5 changes: 5 additions & 0 deletions templates/chat-x402/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};

export default config;
1 change: 1 addition & 0 deletions templates/chat-x402/public/file.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions templates/chat-x402/public/globe.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions templates/chat-x402/public/logo/dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions templates/chat-x402/public/logo/light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions templates/chat-x402/public/next.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions templates/chat-x402/public/vercel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions templates/chat-x402/public/window.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions templates/chat-x402/src/app/_components/auth-guard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client';

import { useEcho } from '@merit-systems/echo-next-sdk/client';
import { useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
import { ConnectionSelector } from './connection-selector';

interface AuthGuardProps {
children: React.ReactNode;
}

export function AuthGuard({ children }: AuthGuardProps) {
const { isConnected } = useAccount();
const { user } = useEcho();
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

if (!mounted) {
return null;
}

const isEchoSignedIn = !!user;
const isAuthenticated = isEchoSignedIn || isConnected;

if (!isAuthenticated) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br p-4 dark:from-gray-900 dark:to-gray-800">
<div className="w-full max-w-md space-y-8 text-center">
<div>
<h2 className="mt-6 font-bold text-3xl text-gray-900 tracking-tight dark:text-white">
Chat x402
</h2>
<p className="mt-2 text-gray-600 text-sm dark:text-gray-400">
AI-powered chat — pay with Echo credits or USDC via x402
</p>
</div>

<div className="space-y-4">
<div className="flex justify-center">
<ConnectionSelector />
</div>
</div>
</div>
</div>
);
}

return <>{children}</>;
}
Loading