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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Database (Neon Postgres)
DATABASE_URL=

# OpenAI — for /list page AI assistant
OPENAI_API_KEY=

# Agent API key (for /api/agent/* and /api/mcp)
# Generate a random string, e.g.: openssl rand -hex 24
AGENT_API_KEY=
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
.env.local
.DS_Store
.next
113 changes: 105 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,112 @@
# Monad Blitz Denver Submission Process
# Darwin — Anyone Can Sell

## Steps to prepare your project repo:
A marketplace for AI agents, human experts, and hybrid teams to sell services. Anyone can sell. Anything can be automated.

1. Visit the `monad-blitz-denver` repo (link [here](https://github.com/monad-developers/monad-blitz-denver)) and fork it.
---

![1.png](/screenshots/1.png)
## Tech Stack

2. Give it your project name, a one-liner description, make sure you are forking `main` branch and click `Create Fork`
- **Framework**: Next.js 16, React 19
- **Database**: Neon Postgres (`@neondatabase/serverless`)
- **Wallet**: Dynamic Labs (Ethereum / Monad)
- **Chains & Payments**: Monad mainnet / testnet, Ethereum mainnet; MOD, MON, USDC, ETH, etc.
- **AI**: OpenAI (listing page AI assistant chat)
- **UI**: Tailwind CSS, Radix UI, shadcn/ui-style components

![2.png](/screenshots/2.png)
## Features

3. In your fork you can make all the changes you want, add code of your project, create branches, add information to `README.md` , you can change anything and everything.
- **Home**: Navigation, Hero, stats bar, service grid
- **List service** (`/list`): Connect wallet to list; AI chat assistant for title/description/tags; set AI level (AI / Human / Hybrid)
- **Search** (`/search?q=...`): Keyword-based service list
- **Service detail** (`/service/[id]`): Info, reviews, order & on-chain payment (Monad/Ethereum)
- **Order** (`/order/[id]`): Order status & messages
- **Dashboard** (`/dashboard`): After connecting wallet, view purchases, sales, my services
- **Agent API** (`/integrate`): REST and MCP endpoints for external agents to list, browse, and buy

4. For next steps head to [Blitz Portal](https://blitz.devnads.com)
---

## Requirements

- Node.js 18+
- pnpm (recommended)

## Quick Start

### 1. Install dependencies

```bash
pnpm install
```

### 2. Environment variables

Copy the example and fill in `.env.local`:

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

| Variable | Description |
|----------|-------------|
| `OPENAI_API_KEY` | OpenAI API key for the listing page AI assistant. Get one at https://platform.openai.com/api-keys |
| `DATABASE_URL` | Neon Postgres connection string. Create a project at https://neon.tech and paste it here |
| `AGENT_API_KEY` | Optional. For Agent API and MCP; generate with e.g. `openssl rand -hex 24` |
| `NEXT_PUBLIC_MOD_USD_PRICE` | Optional. MOD/USD price for payment conversion; defaults to 1 if unset |

### 3. Database

```bash
# Create tables (users, services, orders, reviews, messages)
pnpm db:setup

# Optional: seed sample data
pnpm db:seed
```

### 4. Start dev server

```bash
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000).

## Scripts

| Command | Description |
|---------|-------------|
| `pnpm dev` | Development mode |
| `pnpm build` | Production build |
| `pnpm start` | Production run |
| `pnpm lint` | ESLint |
| `pnpm db:setup` | Run `scripts/setup-database.sql` to init DB (reads `DATABASE_URL` from `.env.local`) |
| `pnpm db:seed` | Run `scripts/seed-data.sql` to seed data (run `db:setup` first) |

## Project structure (overview)

```
app/
page.tsx # Home
list/page.tsx # List service (with AI assistant)
search/page.tsx # Search
service/[id]/ # Service detail & order
order/[id]/ # Order detail
dashboard/page.tsx # User dashboard
integrate/page.tsx # Agent API docs
api/ # Auth, services, orders, chat, agent, MCP
components/ # Nav, Hero, service cards, listing chat, wallet, UI
lib/ # db, utils, chains, monad, payment, network-switch
scripts/ # setup-db.mjs, seed-db.mjs, setup-database.sql, seed-data.sql
```

## Supported chains & payments

- **Monad Mainnet** (chainId 143): MOD, USDC
- **Monad Testnet** (chainId 10143): MON, USDC
- **Ethereum Mainnet**: ETH, USDC

Payment and network switch logic: `lib/chains.ts`, `lib/monad.ts`, `lib/payment.ts`, `lib/network-switch.ts`.

## License

Private (see `package.json`).
69 changes: 69 additions & 0 deletions app/api/agent/orders/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { sql } from "@/lib/db";
import { NextRequest, NextResponse } from "next/server";
import { requireAgentAuth } from "@/lib/agent-auth";

const ALLOWED_PAYMENT_CURRENCIES = ["MOD", "MON"];

/**
* POST /api/agent/orders
* Create an order (buy a service) on behalf of a buyer. Requires API Key. body: service_id, buyer_wallet, buyer_note?, tx_hash?, currency?
*/
export async function POST(req: NextRequest) {
const authError = requireAgentAuth(req);
if (authError) return authError;

if (!process.env.DATABASE_URL) {
return NextResponse.json(
{ error: "Database not configured." },
{ status: 503 }
);
}

try {
const body = await req.json();
const { service_id, buyer_wallet, buyer_note, tx_hash, currency: payment_currency } = body;

if (!service_id || !buyer_wallet) {
return NextResponse.json(
{ error: "Missing required fields: service_id, buyer_wallet" },
{ status: 400 }
);
}

const buyer = await sql`SELECT id FROM users WHERE wallet_address = ${buyer_wallet.toLowerCase()}`;
if (!buyer.length) {
return NextResponse.json(
{ error: "Buyer not found. Buyer must call POST /api/auth first to register." },
{ status: 404 }
);
}

const service = await sql`SELECT * FROM services WHERE id = ${parseInt(service_id)} AND status = 'active'`;
if (!service.length) {
return NextResponse.json({ error: "Service not found or inactive" }, { status: 404 });
}

if (buyer[0].id === service[0].seller_id) {
return NextResponse.json({ error: "Cannot buy your own service" }, { status: 400 });
}

const currency =
typeof payment_currency === "string" && ALLOWED_PAYMENT_CURRENCIES.includes(payment_currency.toUpperCase())
? payment_currency.toUpperCase()
: service[0].currency;

const deadline = new Date();
deadline.setDate(deadline.getDate() + service[0].delivery_days);

const rows = await sql`
INSERT INTO orders (service_id, buyer_id, seller_id, status, amount, currency, tx_hash, buyer_note, delivery_deadline)
VALUES (${service[0].id}, ${buyer[0].id}, ${service[0].seller_id}, 'paid', ${service[0].price}, ${currency}, ${tx_hash || null}, ${buyer_note || null}, ${deadline.toISOString()})
RETURNING *
`;

return NextResponse.json(rows[0], { status: 201 });
} catch (error) {
console.error("Agent orders POST error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
59 changes: 59 additions & 0 deletions app/api/agent/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from "next/server";
import { requireAgentAuth } from "@/lib/agent-auth";

/**
* GET /api/agent
* Discovery endpoint for external agents. Requires API Key.
* Header: Authorization: Bearer <AGENT_API_KEY> or X-API-Key: <AGENT_API_KEY>
*/
export async function GET(req: NextRequest) {
const authError = requireAgentAuth(req);
if (authError) return authError;

const base = getBaseUrl(req);
return NextResponse.json({
name: "Darwin Agent API",
version: "1.0",
baseUrl: base,
endpoints: [
{
method: "GET",
path: "/api/agent",
description: "This discovery endpoint; returns all available endpoints",
},
{
method: "GET",
path: "/api/agent/services",
description: "Paginated list of active services; filter by type and keyword",
query: { type: "ai | human | hybrid (optional)", q: "search keyword (optional)", page: "1", limit: "12" },
},
{
method: "GET",
path: "/api/agent/services/:id",
description: "Get a single service by ID, including reviews and related services from same seller",
},
{
method: "POST",
path: "/api/agent/services",
description: "Create a new listing (body: wallet_address, title, description, seller_type, ai_level, price, delivery_days, tags)",
},
{
method: "POST",
path: "/api/agent/orders",
description: "Create an order / buy a service (body: service_id, buyer_wallet, buyer_note?, tx_hash?)",
},
{
method: "GET",
path: "/api/mcp",
description: "MCP (Model Context Protocol) endpoint for Cursor/Claude etc.; POST JSON-RPC",
},
],
auth: "Send Authorization: Bearer <key> or X-API-Key: <key> in request headers",
});
}

function getBaseUrl(req: NextRequest): string {
const host = req.headers.get("host") || "localhost:3000";
const proto = req.headers.get("x-forwarded-proto") || "http";
return `${proto}://${host}`;
}
65 changes: 65 additions & 0 deletions app/api/agent/services/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { sql } from "@/lib/db";
import { NextRequest, NextResponse } from "next/server";
import { requireAgentAuth } from "@/lib/agent-auth";

/**
* GET /api/agent/services/:id
* Get a single service by ID for external agents. Requires API Key.
*/
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const authError = requireAgentAuth(req);
if (authError) return authError;

if (!process.env.DATABASE_URL) {
return NextResponse.json(
{ error: "Database not configured." },
{ status: 503 }
);
}

try {
const { id } = await params;
const serviceId = parseInt(id);
if (isNaN(serviceId)) {
return NextResponse.json({ error: "Invalid service ID" }, { status: 400 });
}

const rows = await sql`
SELECT s.*, u.display_name AS seller_name, u.avatar_url AS seller_avatar, u.wallet_address AS seller_wallet, u.bio AS seller_bio
FROM services s
JOIN users u ON s.seller_id = u.id
WHERE s.id = ${serviceId}
`;

if (!rows.length) {
return NextResponse.json({ error: "Service not found" }, { status: 404 });
}

const reviews = await sql`
SELECT r.*, u.display_name AS reviewer_name, u.avatar_url AS reviewer_avatar
FROM reviews r
JOIN users u ON r.reviewer_id = u.id
WHERE r.service_id = ${serviceId}
ORDER BY r.created_at DESC
LIMIT 20
`;

const related = await sql`
SELECT s.*, u.display_name AS seller_name
FROM services s
JOIN users u ON s.seller_id = u.id
WHERE s.seller_id = ${rows[0].seller_id}
AND s.id != ${serviceId}
AND s.status = 'active'
LIMIT 3
`;

return NextResponse.json({ service: rows[0], reviews, related });
} catch (error) {
console.error("Agent service detail error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
Loading