Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ─── Supabase ────────────────────────────────────────────────────────────────
NEXT_PUBLIC_SUPABASE_URL=https://jeknglgnsekonctpdzoj.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Impla25nbGduc2Vrb25jdHBkem9qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ3MTIxMDksImV4cCI6MjA5MDI4ODEwOX0.mHv6Sa67F34DsZJ8bhFB9MmdCex0BUMuntyxti_1RJY
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Impla25nbGduc2Vrb25jdHBkem9qIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3NDcxMjEwOSwiZXhwIjoyMDkwMjg4MTA5fQ.B2rXWQILBE7rlo0MqqYe0wP8DCAM4xjxMZofnaDNFmk

# ─── AI Providers ─────────────────────────────────────────────────────────────
# Replace with your actual keys from https://platform.openai.com and https://console.anthropic.com
OPENAI_API_KEY=sk-proj-REPLACE_WITH_YOUR_OPENAI_KEY
ANTHROPIC_API_KEY=sk-ant-api03-REPLACE_WITH_YOUR_ANTHROPIC_KEY

# ─── Stellar / Horizon ────────────────────────────────────────────────────────
NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org
NEXT_PUBLIC_STELLAR_NETWORK=testnet
NEXT_PUBLIC_SOROBAN_CONTRACT_ID=CDTLE6RKAXDXMKDTBLNXYQFIDQKXFM4ARYBX6DG6XDHJPXRESIJEL3MU
# Server-side agent payout wallet (Stellar secret key – never expose to client)
STELLAR_AGENT_SECRET=C22G2DCY443NWBCIMTBA4CSUGTZ4T5HLCCLWJDRCKGMSMNG2KEL6QB7

# ─── Ably ─────────────────────────────────────────────────────────────────────
ABLY_API_KEY=ROcuyw.LBHhOw:ISGI3LICCje39yFL3_GmbI59F9WYrIH4QarATaIBasA
NEXT_PUBLIC_ABLY_KEY=ROcuyw.e5IGqA:sicRpIx7Mur02SD3h0L_4UK4UlDHC9s-kkyOVsBf3wU

# ─── Upstash QStash ───────────────────────────────────────────────────────────
QSTASH_TOKEN=eyJVc2VySUQiOiJmZDZhMGE0ZC1lMmY5LTQ3NWUtYmIzNi05MzVjYzE2MzU3MmUiLCJQYXNzd29yZCI6IjVhMGQzYTIwMDUxZTRhMDNiNDAwNzVhNTA1NzQyZmQzIn0=
QSTASH_CURRENT_SIGNING_KEY=sig_6eSvX3dugsPRfhbSbnB5LG2AnwL3
QSTASH_NEXT_SIGNING_KEY=sig_5EmSqiKD7LDaQGBXQDTKX1k5dapo
16 changes: 11 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up Node 20
- name: Set up Node 22
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'

- name: Upgrade npm to v11
run: npm install -g npm@11

- name: Install dependencies
run: npm ci

Expand All @@ -51,12 +54,15 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up Node 20
- name: Set up Node 22
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'

- name: Upgrade npm to v11
run: npm install -g npm@11

- name: Install dependencies
run: npm ci

Expand Down Expand Up @@ -106,7 +112,7 @@ jobs:
# - uses: actions/checkout@v4
# - uses: actions/setup-node@v4
# with:
# node-version: '20'
# node-version: '22'
# cache: 'npm'
# - run: npm ci
# - run: npx vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
# local env files — .env.local is intentionally tracked for development
# (rotate all keys before moving to production)
.env

# vercel
Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# ── Stage 1: deps ─────────────────────────────────────────────────────────────
FROM node:20-alpine AS deps
FROM node:22-slim AS deps
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev
RUN npm install -g npm@11 && npm ci --omit=dev

# ── Stage 2: builder ───────────────────────────────────────────────────────────
FROM node:20-alpine AS builder
FROM node:22-slim AS builder
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci
RUN npm install -g npm@11 && npm ci

COPY . .

Expand All @@ -29,7 +29,7 @@ ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# ── Stage 3: runner ────────────────────────────────────────────────────────────
FROM node:20-alpine AS runner
FROM node:22-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
Expand Down
69 changes: 64 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Users connect their Freighter wallet, build custom AI agents, monetize them on-c
## Tech Stack

- **Frontend**: Next.js 15 (App Router), TypeScript, TailwindCSS, Framer Motion
- **Wallet**: Stellar Freighter Wallet SDK (`@stellar/freighter-api`)
- **Blockchain**: Stellar network — smart contracts in Soroban (Rust)
- **Wallet**: Multi-wallet support via `WalletConnect` modal — Freighter, LOBSTR, xBull, Albedo
- **Blockchain**: Stellar network — two Soroban smart contracts (AgentRegistry + AgentValidator) with inter-contract calls
- **Database**: Supabase (PostgreSQL)
- **AI Backends**: OpenAI GPT-4o-mini + Anthropic Claude Haiku
- **Payments**: 0x402 protocol for per-request payments in XLM
Expand Down Expand Up @@ -91,19 +91,75 @@ Open [http://localhost:3000](http://localhost:3000).
4. Client retries with `X-Payment-Tx-Hash` header
5. Server verifies via Stellar Horizon API and runs agent

## Smart Contract
## Smart Contracts

The `AgentRegistry` Soroban contract is in `contracts/agent_registry/`. Deploy with:
Two Soroban contracts live in `contracts/`. Deploy both with:

```bash
chmod +x contracts/deploy.sh
./contracts/deploy.sh
```

### AgentRegistry (`contracts/agent_registry/`)

Core on-chain registry. Stores agent metadata, counts requests, and emits events for every paid call.

| Method | Description |
|---|---|
| `register_agent(owner, agent_id, price_xlm, metadata_hash)` | Register a new agent |
| `pay_for_request(caller, agent_id, amount)` | Record a paid 0x402 request |
| `fork_agent(caller, original_id, new_id)` | Fork an existing agent |
| `get_agent(agent_id)` | Read agent data |
| `update_price(owner, agent_id, new_price)` | Update per-request price |

### AgentValidator (`contracts/agent_validator/`)

Deployment gatekeeper that makes **inter-contract calls** to AgentRegistry.

| Method | Description |
|---|---|
| `initialize(admin, registry)` | Link validator to AgentRegistry |
| `validate_wallet(deployer, agent_id)` | **Inter-contract READ** — checks registry for duplicate agent_id |
| `request_deploy(deployer, agent_id, metadata_hash, price_stroops)` | Store pending deployment intent on-chain |
| `confirm_deploy(deployer, agent_id, signature_hash)` | **Inter-contract WRITE** — calls `AgentRegistry::register_agent` |
| `is_confirmed(agent_id)` | Check if agent passed on-chain validation |
| `get_pending(agent_id)` | Read pending deployment record |

### Inter-Contract Call Architecture

```
Browser / CLI
POST /api/agents/validate-deploy ← builds Soroban XDR tx
│ returns unsigned tx XDR
Freighter Wallet (user signs)
│ returns signed XDR
POST /api/agents/confirm-deploy ← submits to Horizon, builds confirm tx
│ returns confirm XDR
Freighter Wallet (user signs confirm tx)
│ signed confirm XDR
Stellar Horizon ── submits tx ──▶ AgentValidator::confirm_deploy
│ env.invoke_contract() ← INTER-CONTRACT CALL
AgentRegistry::register_agent
Agent registered on-chain ✅
```

**Proof of inter-contract call** — the Soroban `env.invoke_contract` / `env.try_invoke_contract` calls in `contracts/agent_validator/src/lib.rs` (the `registry_client` module) are the on-chain cross-contract dispatch. The `confirm_deploy` entry point cannot succeed without successfully calling `AgentRegistry::register_agent`.

> **Contract IDs (Testnet)**
> | Contract | ID |
> |---|---|
> | AgentRegistry | *(set after first deploy — see `contracts/deploy.sh` output)* |
> | AgentRegistry | `CDTLE6RKAXDXMKDTBLNXYQFIDQKXFM4ARYBX6DG6XDHJPXRESIJEL3MU` |
> | AgentValidator | *(set after deploy — see `NEXT_PUBLIC_SOROBAN_VALIDATOR_ID`)* |

## Demo Agent IDs

Expand All @@ -123,7 +179,10 @@ chmod +x contracts/deploy.sh
- `GET /api/agents/list` — List public agents
- `GET /api/agents/[id]` — Get agent by ID
- `POST /api/agents/[id]/run` — Run agent (0x402)
- `POST /api/agents/validate-deploy` — Build Soroban validation transaction XDR
- `POST /api/agents/confirm-deploy` — Submit + build confirm_deploy transaction XDR
- `POST /api/payment/verify` — Verify Stellar tx
- `GET /api/dashboard/analytics` — Dashboard PnL + 0x402 request metrics
- `GET /api/ably/token` — Ably auth token (returns 503 gracefully when not configured)

## Docker
Expand Down
6 changes: 2 additions & 4 deletions app/api/agents/[id]/run/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,10 @@ export async function POST(
caller_wallet: callerWallet || null,
caller_ip: req.headers.get('x-forwarded-for') || null,
input_payload: { input },
output_response: { output },
output_payload: { output },
payment_tx_hash: paymentTxHash,
tx_explorer_url: paymentTxHash ? explorerUrl(paymentTxHash) : null,
payment_amount_xlm: paymentTxHash ? agent.price_xlm : 0,
protocol: '0x402',
status: 'success',
latency_ms: latencyMs,
});
Expand All @@ -222,9 +221,8 @@ export async function POST(
amount_xlm: agent.price_xlm,
tx_hash: paymentTxHash,
tx_explorer_url: txExplorerUrl,
status: 'paid',
},
{ onConflict: 'tx_hash' }
{ onConflict: 'request_id' }
);
}

Expand Down
121 changes: 121 additions & 0 deletions app/api/agents/confirm-deploy/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { NextRequest, NextResponse } from 'next/server';
import * as StellarSdk from 'stellar-sdk';
import crypto from 'node:crypto';

const HORIZON_URL = process.env.NEXT_PUBLIC_HORIZON_URL || 'https://horizon-testnet.stellar.org';
const NETWORK_PASSPHRASE =
process.env.NEXT_PUBLIC_STELLAR_NETWORK === 'mainnet'
? StellarSdk.Networks.PUBLIC
: StellarSdk.Networks.TESTNET;

/**
* POST /api/agents/confirm-deploy
*
* Final step of the on-chain deployment flow:
* 1. Submit the signed validate_wallet + request_deploy transaction to Horizon.
* 2. Build a second transaction for AgentValidator.confirm_deploy (with the
* SHA-256 of the signed validation message as the signature_hash).
* 3. Return the XDR of the confirm_deploy transaction for the client to sign
* and submit.
*
* After the confirm_deploy transaction lands on-chain, AgentValidator makes an
* inter-contract call to AgentRegistry.register_agent — completing the
* fully on-chain deployment flow.
*/
export async function POST(req: NextRequest) {
try {
const body = await req.json() as {
signed_tx_xdr: string;
deployer_wallet: string;
agent_id: string;
validation_message: string;
};

const { signed_tx_xdr, deployer_wallet, agent_id, validation_message } = body;

if (!signed_tx_xdr || !deployer_wallet || !agent_id || !validation_message) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}

const validatorContractId = process.env.NEXT_PUBLIC_SOROBAN_VALIDATOR_ID;

// ── Submit the validate + request_deploy transaction ──────────────────────
const server = new StellarSdk.Horizon.Server(HORIZON_URL);

if (validatorContractId) {
try {
const tx = StellarSdk.TransactionBuilder.fromXDR(signed_tx_xdr, NETWORK_PASSPHRASE);
await server.submitTransaction(tx as StellarSdk.Transaction);
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
return NextResponse.json(
{ error: 'Failed to submit validation transaction to Horizon', details: detail },
{ status: 400 }
);
}
}

// ── Compute signature_hash ────────────────────────────────────────────────
// SHA-256 of the validation message, stored on-chain as proof of consent.
const sigHashBytes = crypto
.createHash('sha256')
.update(validation_message, 'utf8')
.digest();
const sigHashHex = sigHashBytes.toString('hex');

if (!validatorContractId) {
// Dev mode: skip on-chain confirm_deploy
return NextResponse.json({
status: 'confirmed_dev_mode',
signature_hash: sigHashHex,
message:
'NEXT_PUBLIC_SOROBAN_VALIDATOR_ID not set — on-chain confirmation skipped in dev mode.',
});
}

// ── Build confirm_deploy transaction ──────────────────────────────────────
let account: StellarSdk.Horizon.AccountResponse;
try {
account = await server.loadAccount(deployer_wallet);
} catch {
return NextResponse.json(
{ error: `Deployer account ${deployer_wallet} not found on Horizon` },
{ status: 400 }
);
}

const agentIdSym = StellarSdk.xdr.ScVal.scvSymbol(Buffer.from(agent_id.slice(0, 32)));
const deployerSc = new StellarSdk.Address(deployer_wallet).toScVal();

// BytesN<32> — encode as scvBytes
const sigHashSc = StellarSdk.xdr.ScVal.scvBytes(sigHashBytes);

const confirmOp = StellarSdk.Operation.invokeContractFunction({
contract: validatorContractId,
function: 'confirm_deploy',
args: [deployerSc, agentIdSym, sigHashSc],
});

const tx = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: NETWORK_PASSPHRASE,
})
.addOperation(confirmOp)
.setTimeout(300)
.build();

const confirmTxXdr = tx.toXDR();

return NextResponse.json({
status: 'pending_confirm_signature',
confirm_tx_xdr: confirmTxXdr,
network_passphrase: NETWORK_PASSPHRASE,
signature_hash: sigHashHex,
message:
'Sign the confirm_deploy transaction to register your agent on-chain via inter-contract call.',
});
} catch (err) {
console.error('[confirm-deploy] Error:', err);
return NextResponse.json({ error: 'Internal server error', details: String(err) }, { status: 500 });
}
}
4 changes: 3 additions & 1 deletion app/api/agents/list/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ export async function GET(req: NextRequest) {
.limit(limit);

if (owner) {
// Dashboard owner view: include all agents (public, private, forked)
query = query.eq('owner_wallet', owner);
} else {
query = query.eq('visibility', 'public');
// Marketplace view: public + forked agents
query = query.in('visibility', ['public', 'forked']);
}

if (model) query = query.eq('model', model);
Expand Down
Loading
Loading