Skip to content

feat(demo): add “CoinSpark” demo under minikit/Coin-your-idea #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
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 minikit/coin-your-idea/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
.env.local
70 changes: 70 additions & 0 deletions minikit/coin-your-idea/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# CoinSpark Demo

This is a demonstration frame built with [MiniKit](https://docs.base.org/builderkits/minikit/overview), [OnchainKit](https://docs.base.org/builderkits/onchainkit/getting-started), and Zora's Coins SDK to show how to mint your ideas on-chain as unique crypto assets.

## Getting Started

1. Install dependencies:

```bash
npm install
# or yarn install
# or pnpm install
# or bun install
```

2. Set up environment variables:

Create a `.env.local` file in the project root (or use the provided `env.example`) and add the following:

```env
# Required for Frame metadata
NEXT_PUBLIC_URL=
NEXT_PUBLIC_VERSION=
NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME=
NEXT_PUBLIC_ICON_URL=
NEXT_PUBLIC_IMAGE_URL=
NEXT_PUBLIC_SPLASH_IMAGE_URL=
NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR=

# Required for Frame account association
FARCASTER_HEADER=
FARCASTER_PAYLOAD=
FARCASTER_SIGNATURE=

# Required for webhooks and background notifications
REDIS_URL=
REDIS_TOKEN=
```

3. Start the development server:

```bash
npm run dev
```

## Features

- AI-powered idea-to-coin generation via OpenAI
- On-chain minting with Zora's Coins SDK
- Frame integration for seamless account association and notifications
- View your minted coins in the “My Coins” tab
- Real-time transaction feedback with Etherscan links
- Dark/light theme support with customizable styling via Tailwind CSS

## Project Structure

- `app/page.tsx` — Main demo page with Create and My Coins tabs
- `app/components/` — Reusable UI components (inputs, cards, modals)
- `app/api/` — Backend routes for coin generation, metadata, and notifications
- `app/providers.tsx` — MiniKit and OnchainKit setup
- `app/theme.css` — Custom theming for Tailwind and OnchainKit
- `.well-known/farcaster.json` — Frame metadata endpoint

## Learn More

- MiniKit Documentation: https://docs.base.org/builderkits/minikit/overview
- OnchainKit Documentation: https://docs.base.org/builderkits/onchainkit/getting-started
- Zora Coins SDK: https://github.com/zoralabs/coins-sdk
- Next.js Documentation: https://nextjs.org/docs
- Tailwind CSS Documentation: https://tailwindcss.com/docs
Binary file added minikit/coin-your-idea/app/.DS_Store
Binary file not shown.
22 changes: 22 additions & 0 deletions minikit/coin-your-idea/app/.well-known/farcaster.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export async function GET() {
const URL = process.env.NEXT_PUBLIC_URL;

return Response.json({
accountAssociation: {
header: process.env.FARCASTER_HEADER,
payload: process.env.FARCASTER_PAYLOAD,
signature: process.env.FARCASTER_SIGNATURE,
},
frame: {
version: process.env.NEXT_PUBLIC_VERSION,
name: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME,
homeUrl: URL,
iconUrl: process.env.NEXT_PUBLIC_ICON_URL,
imageUrl: process.env.NEXT_PUBLIC_IMAGE_URL,
buttonTitle: `Launch ${process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME}`,
splashImageUrl: process.env.NEXT_PUBLIC_SPLASH_IMAGE_URL,
splashBackgroundColor: `#${process.env.NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR}`,
webhookUrl: `${URL}/api/webhook`,
},
});
}
Binary file added minikit/coin-your-idea/app/api/.DS_Store
Binary file not shown.
54 changes: 54 additions & 0 deletions minikit/coin-your-idea/app/api/coin-metadata/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server';
import { PROJECT_URL } from '@/lib/constants';

// Simple in-memory store for demo purposes
const metadataStore: Record<string, any> = {};

export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const id = params.id;
const data = await request.json();

// Store metadata with environment-specific image URL
metadataStore[id] = {
name: data.name,
symbol: data.symbol,
description: data.description,
image: process.env.ENV === 'local'
? "https://i.imgur.com/tdvjX6c.png"
: `${PROJECT_URL}/api/generateImage?idea=${data.description}`
};

return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: 'Failed to store metadata' }, { status: 500 });
}
}

export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const id = params.id;

// Return stored metadata or fallback with environment-specific image URL
if (metadataStore[id]) {
return NextResponse.json(metadataStore[id]);
}

return NextResponse.json({
name: "Banger Coin",
symbol: "BANGER",
description: "A coin created from a banger",
image: process.env.ENV === 'local'
? "https://i.imgur.com/tdvjX6c.png"
: `${PROJECT_URL}/api/generateImage?idea=A coin created from a banger`
});
} catch (error) {
return NextResponse.json({ error: 'Failed to retrieve metadata' }, { status: 500 });
}
}
92 changes: 92 additions & 0 deletions minikit/coin-your-idea/app/api/generate-coin/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';
import crypto from 'crypto';
import clientPromise from '@/lib/mongodb';

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // Server-side environment variable
});

export async function POST(request: NextRequest) {
try {
const { idea, owner } = await request.json();
if (!idea || !owner) {
return NextResponse.json({ error: 'Idea and owner address required' }, { status: 400 });
}

const result = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content: `Generate parameters for a cryptocurrency coin based on the following idea.\nReturn JSON in this format:\n{\n "name": "short name (max 3 words)",\n "symbol": "ticker symbol (max 5 letters)"\n}`
},
{ role: "user", content: idea }
]
});

const content = result.choices[0].message.content;
let parsedContent;

try {
parsedContent = JSON.parse(content || '{}');
} catch (e) {
parsedContent = {
name: idea.split(' ').slice(0, 3).join(' ').substring(0, 20) || "Idea Coin",
symbol: idea.split(' ')
.filter((w: string) => w)
.slice(0, 3)
.map((word: string) => word[0])
.join('')
.toUpperCase()
.substring(0, 5) || "IDEA"
};
}

// Generate unique ID
const uniqueId = crypto.randomBytes(8).toString('hex');

// Create metadata URL using request origin
const origin = new URL(request.url).origin;
const metadataUrl = `${origin}/api/coin-metadata/${uniqueId}`;

// Store metadata
await fetch(metadataUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: parsedContent.name,
symbol: parsedContent.symbol,
description: idea,
}),
});

// Insert coin record into MongoDB
const client = await clientPromise;
const db = client.db();
await db.collection('coins').insertOne({
id: uniqueId,
name: parsedContent.name,
symbol: parsedContent.symbol,
description: idea,
metadataUrl,
ownerAddress: owner,
createdAt: new Date()
});

return NextResponse.json({
name: parsedContent.name,
symbol: parsedContent.symbol,
metadataUrl
});

} catch (error) {
console.error('Error generating coin parameters:', error);
return NextResponse.json(
{ error: 'Failed to generate coin parameters' },
{ status: 500 }
);
}
}
50 changes: 50 additions & 0 deletions minikit/coin-your-idea/app/api/generateImage/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const idea = searchParams.get('idea') || 'Default idea';

return new ImageResponse(
(
<div
style={{
display: 'flex',
flexDirection: 'column',
fontSize: 40,
color: 'white',
background: 'linear-gradient(to bottom right, #663399, #FF6B6B)',
width: '100%',
height: '100%',
padding: 40,
textAlign: 'center',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '15px',
}}
>
<div style={{ fontSize: 60, fontWeight: 'bold', marginBottom: 20 }}>This is a Coined Idea</div>
<div style={{
padding: '20px',
background: 'rgba(0,0,0,0.3)',
borderRadius: '10px',
maxWidth: '80%',
wordWrap: 'break-word'
}}>
{idea}
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
} catch (error) {
console.error('Error generating image:', error);
return new Response('Error generating image', { status: 500 });
}
}
53 changes: 53 additions & 0 deletions minikit/coin-your-idea/app/api/metadata/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';

// In-memory store for demonstration (in production you'd use a database)
const metadataStore: Record<string, any> = {};

export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const id = params.id;

// Check if we already have this metadata stored
if (metadataStore[id]) {
return NextResponse.json(metadataStore[id]);
}

// If not, this is a new request - decode the ID
const decodedIdea = decodeURIComponent(Buffer.from(id, 'base64').toString());

// Get the host for creating absolute URLs
const host = request.headers.get('host') || 'localhost:3000';
const protocol = host.includes('localhost') ? 'http' : 'https';
const baseUrl = `${protocol}://${host}`;

// Create image URL
const imageUrl = `${baseUrl}/api/generateImage?idea=${encodeURIComponent(decodedIdea)}`;

// Create coin metadata following EIP-7572 standard
const metadata = {
name: decodedIdea.split(' ').slice(0, 3).join(' ').substring(0, 30) || "Idea Coin",
description: decodedIdea.substring(0, 500) || "A fun idea coin",
symbol: decodedIdea.split(' ')
.slice(0, 3)
.map(word => word[0])
.join('')
.toUpperCase()
.substring(0, 5) || "IDEA",
image: imageUrl
};

// Store for future requests
metadataStore[id] = metadata;

return NextResponse.json(metadata);
} catch (error) {
console.error('Error generating metadata:', error);
return NextResponse.json(
{ error: 'Failed to generate metadata' },
{ status: 500 }
);
}
}
25 changes: 25 additions & 0 deletions minikit/coin-your-idea/app/api/my-coins/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from 'next/server';
import clientPromise from '@/lib/mongodb';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const owner = searchParams.get('owner');
if (!owner) {
return NextResponse.json({ error: 'Owner address is required' }, { status: 400 });
}

const client = await clientPromise;
const db = client.db();
const coins = await db
.collection('coins')
.find({ ownerAddress: owner })
.sort({ createdAt: -1 })
.toArray();

return NextResponse.json(coins);
} catch (error) {
console.error('Error fetching coins:', error);
return NextResponse.json({ error: 'Failed to fetch coins' }, { status: 500 });
}
}
Loading