diff --git a/agents-sdk/templates/python/README.md b/agents-sdk/templates/python/README.md new file mode 100644 index 0000000..a2ea1d6 --- /dev/null +++ b/agents-sdk/templates/python/README.md @@ -0,0 +1,58 @@ +# AgentForge Python Agent Templates (LangGraph) + +These templates provide LangGraph-powered agent workflows for all 6 AgentForge agent types. + +## Prerequisites + +```bash +pip install -r requirements.txt +``` + +Set environment variables in `.env`: +``` +AGENTFORGE_API_URL=http://localhost:3000 +OPENAI_API_KEY=your_key +ANTHROPIC_API_KEY=your_key +STELLAR_AGENT_SECRET=your_secret +ABLY_API_KEY=your_key +``` + +## Agents + +| Template | Agent | Description | +|----------|-------|-------------| +| `mev_bot_agent.py` | MEV Bot | Front-running & sandwich detection | +| `arbitrage_tracker_agent.py` | Arbitrage Tracker | Triangular cross-path arbitrage | +| `trading_bot_agent.py` | Trading Bot | Grid, DCA, trend strategies | +| `mempool_monitor_agent.py` | Mempool Monitor | Real-time transaction stream analysis | +| `relayer_agent.py` | Relayer | Fee-bump relay with 0x402 charging | +| `liquidity_tracker_agent.py` | Liquidity Tracker | Order-book depth & slippage simulation | + +## Multi-Agent (A2A) Example + +```python +from base_agent import build_a2a_graph + +# Chain MEV Bot → Trading Bot +app = build_a2a_graph( + agent1_id="mev_bot", + agent2_id="trading_bot", + system_prompt1="MEV detection prompt...", + system_prompt2="Trading execution prompt...", +) +result = app.invoke({"input": "Find and execute best MEV opportunity", ...}) +``` + +## 0x402 Payment Flow + +When an agent requires payment: +1. First call returns `payment_required=True` with `payment_amount` and `payment_address` +2. Use `stellar-sdk` to submit XLM payment to `payment_address` +3. Retry with `tx_hash` set to the transaction hash + +## CLI Integration + +```bash +agentforge agents run mev_bot --input "scan for opportunities" --secret $STELLAR_SECRET +agentforge a2a call mev_bot trading_bot --input "find and execute MEV" --secret $STELLAR_SECRET +``` diff --git a/agents-sdk/templates/python/arbitrage_tracker_agent.py b/agents-sdk/templates/python/arbitrage_tracker_agent.py new file mode 100644 index 0000000..0295024 --- /dev/null +++ b/agents-sdk/templates/python/arbitrage_tracker_agent.py @@ -0,0 +1,31 @@ +""" +AgentForge LangGraph Template — Arbitrage Tracker Agent +Triangular & cross-path arbitrage across Stellar DEX. +""" +import json +from base_agent import build_single_agent_graph, AgentState + +SYSTEM_PROMPT = """You are an arbitrage tracking agent for the Stellar DEX. +Your tasks: +1. Monitor triangular arbitrage opportunities across XLM, USDC, BTC, ETH pairs +2. Calculate profit margins after transaction fees and slippage +3. Identify optimal arbitrage paths with lowest risk +4. Track historical arbitrage performance and success rates +5. Alert on opportunities above 0.5% profit threshold + +Provide structured analysis with: pair paths, expected profit %, execution risk, and recommended action. +""" + +def run_arbitrage_tracker(input_prompt: str, wallet_address: str = "", tx_hash: str = None): + app = build_single_agent_graph(agent_id="arb_tracker", system_prompt=SYSTEM_PROMPT) + state: AgentState = { + "input": input_prompt, "output": "", "agent_id": "arb_tracker", + "wallet_address": wallet_address, "tx_hash": tx_hash, + "payment_required": False, "payment_amount": 0.0, "payment_address": "", + "error": None, "steps": [], + } + return app.invoke(state) + +if __name__ == "__main__": + result = run_arbitrage_tracker("Find arbitrage opportunities for XLM/USDC/BTC triangle") + print(json.dumps(result, indent=2)) diff --git a/agents-sdk/templates/python/base_agent.py b/agents-sdk/templates/python/base_agent.py new file mode 100644 index 0000000..9b1a32a --- /dev/null +++ b/agents-sdk/templates/python/base_agent.py @@ -0,0 +1,164 @@ +""" +AgentForge LangGraph Base Agent Template +Provides the common 0x402 payment-gated LangGraph workflow pattern. +""" + +import os +import json +import time +from typing import TypedDict, Optional, List +from dotenv import load_dotenv +from langgraph.graph import StateGraph, END +from langchain_openai import ChatOpenAI +from langchain_anthropic import ChatAnthropic +from langchain.schema import HumanMessage, SystemMessage +import requests + +load_dotenv() + +AGENTFORGE_API_URL = os.getenv("AGENTFORGE_API_URL", "http://localhost:3000") +STELLAR_AGENT_SECRET = os.getenv("STELLAR_AGENT_SECRET", "") +ABLY_API_KEY = os.getenv("ABLY_API_KEY", "") + + +class AgentState(TypedDict): + input: str + output: str + agent_id: str + wallet_address: str + tx_hash: Optional[str] + payment_required: bool + payment_amount: float + payment_address: str + error: Optional[str] + steps: List[str] + + +def build_model(model_name: str = "openai-gpt4o-mini"): + """Build a LangChain model from the model name.""" + if model_name == "openai-gpt4o-mini": + return ChatOpenAI(model="gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY")) + elif model_name == "anthropic-claude-haiku": + return ChatAnthropic(model="claude-haiku-20240307", api_key=os.getenv("ANTHROPIC_API_KEY")) + else: + return ChatOpenAI(model="gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY")) + + +def create_run_node(agent_id: str, system_prompt: str, model_name: str = "openai-gpt4o-mini"): + """Create a LangGraph node that runs an agent via the 0x402 API.""" + model = build_model(model_name) + + def run_node(state: AgentState) -> AgentState: + headers = {"Content-Type": "application/json"} + if state.get("wallet_address"): + headers["X-Payment-Wallet"] = state["wallet_address"] + if state.get("tx_hash"): + headers["X-Payment-Tx-Hash"] = state["tx_hash"] + + try: + resp = requests.post( + f"{AGENTFORGE_API_URL}/api/agents/{agent_id}/run", + headers=headers, + json={"input": state["input"]}, + timeout=30, + ) + data = resp.json() + + if resp.status_code == 402: + pd = data.get("payment_details", {}) + return { + **state, + "payment_required": True, + "payment_amount": pd.get("amount_xlm", 0), + "payment_address": pd.get("address", ""), + "steps": state.get("steps", []) + ["payment_required"], + } + + if not resp.ok or data.get("error"): + return { + **state, + "error": data.get("error", f"HTTP {resp.status_code}"), + "steps": state.get("steps", []) + [f"error:{resp.status_code}"], + } + + return { + **state, + "output": data.get("output", ""), + "payment_required": False, + "steps": state.get("steps", []) + ["completed"], + } + except Exception as e: + return {**state, "error": str(e), "steps": state.get("steps", []) + ["exception"]} + + return run_node + + +def should_retry_payment(state: AgentState) -> str: + if state.get("error"): + return "error" + if state.get("payment_required"): + return "payment_required" + return "completed" + + +def build_single_agent_graph(agent_id: str, system_prompt: str, model_name: str = "openai-gpt4o-mini") -> StateGraph: + """Build a simple single-agent LangGraph workflow.""" + run_node = create_run_node(agent_id, system_prompt, model_name) + + graph = StateGraph(AgentState) + graph.add_node("run", run_node) + graph.set_entry_point("run") + graph.add_edge("run", END) + + return graph.compile() + + +def build_a2a_graph( + agent1_id: str, + agent2_id: str, + system_prompt1: str, + system_prompt2: str, + model_name: str = "openai-gpt4o-mini", +) -> StateGraph: + """Build a multi-agent A2A LangGraph workflow where agent1 feeds agent2.""" + run1 = create_run_node(agent1_id, system_prompt1, model_name) + run2 = create_run_node(agent2_id, system_prompt2, model_name) + + def bridge_node(state: AgentState) -> AgentState: + """Pass agent1 output as agent2 input.""" + return { + **state, + "input": f"[Agent 1 Output]: {state['output']}\n\n[Original Task]: {state['input']}", + } + + graph = StateGraph(AgentState) + graph.add_node("run_agent1", run1) + graph.add_node("bridge", bridge_node) + graph.add_node("run_agent2", run2) + graph.set_entry_point("run_agent1") + graph.add_edge("run_agent1", "bridge") + graph.add_edge("bridge", "run_agent2") + graph.add_edge("run_agent2", END) + + return graph.compile() + + +if __name__ == "__main__": + # Example: run a single agent + agent_app = build_single_agent_graph( + agent_id="1", + system_prompt="You are a DeFi analyst.", + ) + result = agent_app.invoke({ + "input": "Analyze current XLM/USDC liquidity", + "output": "", + "agent_id": "1", + "wallet_address": "", + "tx_hash": None, + "payment_required": False, + "payment_amount": 0.0, + "payment_address": "", + "error": None, + "steps": [], + }) + print(json.dumps(result, indent=2)) diff --git a/agents-sdk/templates/python/liquidity_tracker_agent.py b/agents-sdk/templates/python/liquidity_tracker_agent.py new file mode 100644 index 0000000..a707114 --- /dev/null +++ b/agents-sdk/templates/python/liquidity_tracker_agent.py @@ -0,0 +1,32 @@ +""" +AgentForge LangGraph Template — Liquidity Slippage Tracker Agent +Order-book depth analysis with real-time slippage simulation. +""" +import json +from base_agent import build_single_agent_graph, AgentState + +SYSTEM_PROMPT = """You are a liquidity and slippage tracking agent for the Stellar DEX. +Your analysis includes: +1. Real-time order book depth for all major Stellar trading pairs +2. Slippage simulation for trades of various sizes (100, 1000, 10000 XLM) +3. Liquidity concentration analysis (bid/ask spread, wall detection) +4. Yield opportunity detection in liquidity pools +5. Optimal trade routing to minimize market impact + +Provide structured data: pair, bid depth, ask depth, slippage at each size tier, +recommended max trade size for <1% slippage, and yield APR if applicable. +""" + +def run_liquidity_tracker(input_prompt: str, wallet_address: str = "", tx_hash: str = None): + app = build_single_agent_graph(agent_id="liquidity_tracker", system_prompt=SYSTEM_PROMPT) + state: AgentState = { + "input": input_prompt, "output": "", "agent_id": "liquidity_tracker", + "wallet_address": wallet_address, "tx_hash": tx_hash, + "payment_required": False, "payment_amount": 0.0, "payment_address": "", + "error": None, "steps": [], + } + return app.invoke(state) + +if __name__ == "__main__": + result = run_liquidity_tracker("Analyze XLM/USDC liquidity depth and simulate 5000 XLM trade slippage") + print(json.dumps(result, indent=2)) diff --git a/agents-sdk/templates/python/mempool_monitor_agent.py b/agents-sdk/templates/python/mempool_monitor_agent.py new file mode 100644 index 0000000..4ab62c5 --- /dev/null +++ b/agents-sdk/templates/python/mempool_monitor_agent.py @@ -0,0 +1,31 @@ +""" +AgentForge LangGraph Template — Mempool Monitor Agent +Real-time Stellar transaction stream analysis via Horizon SSE. +""" +import json +from base_agent import build_single_agent_graph, AgentState + +SYSTEM_PROMPT = """You are a mempool monitoring agent for the Stellar network. +Your responsibilities: +1. Analyze pending Stellar transactions from the Horizon SSE stream +2. Detect unusually large transactions (>100,000 XLM) +3. Identify smart contract interactions with Soroban +4. Alert on potential wash trading or market manipulation +5. Track transaction fee trends and network congestion + +Provide real-time alerts with: transaction hash, amount, type, risk level, and recommended action. +""" + +def run_mempool_monitor(input_prompt: str, wallet_address: str = "", tx_hash: str = None): + app = build_single_agent_graph(agent_id="mempool_monitor", system_prompt=SYSTEM_PROMPT) + state: AgentState = { + "input": input_prompt, "output": "", "agent_id": "mempool_monitor", + "wallet_address": wallet_address, "tx_hash": tx_hash, + "payment_required": False, "payment_amount": 0.0, "payment_address": "", + "error": None, "steps": [], + } + return app.invoke(state) + +if __name__ == "__main__": + result = run_mempool_monitor("Monitor Stellar mempool for large transactions in the last 5 minutes") + print(json.dumps(result, indent=2)) diff --git a/agents-sdk/templates/python/mev_bot_agent.py b/agents-sdk/templates/python/mev_bot_agent.py new file mode 100644 index 0000000..3af25b6 --- /dev/null +++ b/agents-sdk/templates/python/mev_bot_agent.py @@ -0,0 +1,41 @@ +""" +AgentForge LangGraph Template — MEV Bot Agent +Front-running & sandwich detection on Stellar DEX with A2A support. +""" +import json +from base_agent import build_single_agent_graph, build_a2a_graph, AgentState + +SYSTEM_PROMPT = """You are an advanced MEV (Maximal Extractable Value) bot operating on the Stellar DEX. +Your tasks: +1. Detect front-running opportunities in the Stellar DEX order book +2. Identify sandwich attack vectors on large pending transactions +3. Calculate optimal trade sizes and slippage tolerances +4. Execute atomic arbitrage within a single ledger when profitable +5. Report all detected opportunities with risk/reward ratios + +Always provide quantitative analysis with entry/exit prices, expected profit in XLM, and confidence levels. +""" + +def run_mev_bot(input_prompt: str, wallet_address: str = "", tx_hash: str = None): + app = build_single_agent_graph( + agent_id="mev_bot", + system_prompt=SYSTEM_PROMPT, + ) + state: AgentState = { + "input": input_prompt, + "output": "", + "agent_id": "mev_bot", + "wallet_address": wallet_address, + "tx_hash": tx_hash, + "payment_required": False, + "payment_amount": 0.0, + "payment_address": "", + "error": None, + "steps": [], + } + return app.invoke(state) + + +if __name__ == "__main__": + result = run_mev_bot("Scan Stellar DEX for MEV opportunities in the last 100 transactions") + print(json.dumps(result, indent=2)) diff --git a/agents-sdk/templates/python/relayer_agent.py b/agents-sdk/templates/python/relayer_agent.py new file mode 100644 index 0000000..42b1e80 --- /dev/null +++ b/agents-sdk/templates/python/relayer_agent.py @@ -0,0 +1,31 @@ +""" +AgentForge LangGraph Template — Relayer Agent +Fee-bump transaction relay with 0x402 micropayment charging. +""" +import json +from base_agent import build_single_agent_graph, AgentState + +SYSTEM_PROMPT = """You are a transaction relayer agent for the Stellar network with 0x402 micropayment charging. +Your functions: +1. Accept unsigned transactions from users and fee-bump them to the network +2. Calculate optimal fee amounts based on network load +3. Queue multiple transactions for batch processing efficiency +4. Track relay success rates and failure reasons +5. Charge micropayments via 0x402 protocol for each relayed transaction + +For each relay request provide: fee estimate, processing time, relay path, and cost breakdown. +""" + +def run_relayer(input_prompt: str, wallet_address: str = "", tx_hash: str = None): + app = build_single_agent_graph(agent_id="relayer", system_prompt=SYSTEM_PROMPT) + state: AgentState = { + "input": input_prompt, "output": "", "agent_id": "relayer", + "wallet_address": wallet_address, "tx_hash": tx_hash, + "payment_required": False, "payment_amount": 0.0, "payment_address": "", + "error": None, "steps": [], + } + return app.invoke(state) + +if __name__ == "__main__": + result = run_relayer("Relay a fee-bump transaction for a gasless user experience") + print(json.dumps(result, indent=2)) diff --git a/agents-sdk/templates/python/requirements.txt b/agents-sdk/templates/python/requirements.txt new file mode 100644 index 0000000..d842dc7 --- /dev/null +++ b/agents-sdk/templates/python/requirements.txt @@ -0,0 +1,7 @@ +langchain>=0.3.0 +langchain-openai>=0.3.0 +langchain-anthropic>=0.3.0 +langgraph>=0.2.0 +python-dotenv>=1.0.0 +requests>=2.31.0 +stellar-sdk>=10.0.0 diff --git a/agents-sdk/templates/python/trading_bot_agent.py b/agents-sdk/templates/python/trading_bot_agent.py new file mode 100644 index 0000000..76bfdef --- /dev/null +++ b/agents-sdk/templates/python/trading_bot_agent.py @@ -0,0 +1,32 @@ +""" +AgentForge LangGraph Template — Trading Bot Agent +Buy/sell/short strategies with grid & DCA modes on Stellar testnet. +""" +import json +from base_agent import build_single_agent_graph, build_a2a_graph, AgentState + +SYSTEM_PROMPT = """You are a professional trading bot for the Stellar DEX testnet. +Strategies available: +1. Grid Trading: Set buy/sell grid levels, auto-rebalance on price movements +2. DCA (Dollar Cost Averaging): Periodic XLM purchases at set intervals +3. Trend Following: Use moving averages to follow market momentum +4. Mean Reversion: Trade when prices deviate from historical averages +5. Stop-Loss/Take-Profit: Automatic position management + +For each trade signal provide: direction (buy/sell/short), pair, entry price, target, stop-loss, +position size, leverage (if applicable), and confidence score. +""" + +def run_trading_bot(input_prompt: str, wallet_address: str = "", tx_hash: str = None): + app = build_single_agent_graph(agent_id="trading_bot", system_prompt=SYSTEM_PROMPT) + state: AgentState = { + "input": input_prompt, "output": "", "agent_id": "trading_bot", + "wallet_address": wallet_address, "tx_hash": tx_hash, + "payment_required": False, "payment_amount": 0.0, "payment_address": "", + "error": None, "steps": [], + } + return app.invoke(state) + +if __name__ == "__main__": + result = run_trading_bot("Set up a grid trading strategy for XLM/USDC between 0.10 and 0.15") + print(json.dumps(result, indent=2)) diff --git a/app/api/agents/compose/route.ts b/app/api/agents/compose/route.ts new file mode 100644 index 0000000..b1cdcdc --- /dev/null +++ b/app/api/agents/compose/route.ts @@ -0,0 +1,114 @@ +/** + * POST /api/agents/compose + * + * Runs two agents in sequence (A2A pattern): agent1 processes the input, + * then its output becomes the input for agent2. + * Each step checks for 0x402 payment via the wallet in the header. + */ +import { NextRequest, NextResponse } from 'next/server'; +import { v4 as uuidv4 } from 'uuid'; +import Ably from 'ably'; +import type { MarketplaceActivityEvent } from '@/types/events'; +import { publish, TOPICS } from '@/lib/qstash'; + +const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + +async function pushToAbly(activity: MarketplaceActivityEvent) { + const key = process.env.ABLY_API_KEY; + if (!key) return; + try { + const ably = new Ably.Rest({ key }); + await ably.channels.get('marketplace').publish(activity.eventType, activity); + } catch { /* ignore */ } + try { + await publish(TOPICS.MARKETPLACE_ACTIVITY, activity); + } catch { /* ignore */ } +} + +export async function POST(req: NextRequest) { + const body = await req.json().catch(() => ({})) as { + agent1Id?: string; + agent2Id?: string; + input?: string; + txHash1?: string; + txHash2?: string; + walletAddress?: string; + }; + + const { agent1Id, agent2Id, input, txHash1, txHash2, walletAddress } = body; + + if (!agent1Id || !agent2Id || !input) { + return NextResponse.json( + { error: 'agent1Id, agent2Id and input are required' }, + { status: 400 } + ); + } + + const correlationId = uuidv4(); + const results: Array<{ agentId: string; output: string; latencyMs: number }> = []; + + // Step 1: Run agent 1 + const headers1: Record = { 'Content-Type': 'application/json' }; + if (walletAddress) headers1['X-Payment-Wallet'] = walletAddress; + if (txHash1) headers1['X-Payment-Tx-Hash'] = txHash1; + + const start1 = Date.now(); + const res1 = await fetch(`${APP_URL}/api/agents/${agent1Id}/run`, { + method: 'POST', + headers: headers1, + body: JSON.stringify({ input }), + }); + + const data1 = await res1.json() as { output?: string; error?: string; payment_details?: unknown }; + if (!res1.ok || data1.error) { + return NextResponse.json( + { error: `Agent 1 failed: ${data1.error || 'unknown'}`, payment_details: data1.payment_details }, + { status: res1.status } + ); + } + + const latency1 = Date.now() - start1; + results.push({ agentId: agent1Id, output: data1.output || '', latencyMs: latency1 }); + + // Step 2: Run agent 2 with agent 1's output as input + const headers2: Record = { 'Content-Type': 'application/json' }; + if (walletAddress) headers2['X-Payment-Wallet'] = walletAddress; + if (txHash2) headers2['X-Payment-Tx-Hash'] = txHash2; + + const composedInput = `[Agent 1 Output]: ${data1.output}\n\n[Original Task]: ${input}`; + const start2 = Date.now(); + const res2 = await fetch(`${APP_URL}/api/agents/${agent2Id}/run`, { + method: 'POST', + headers: headers2, + body: JSON.stringify({ input: composedInput }), + }); + + const data2 = await res2.json() as { output?: string; error?: string; payment_details?: unknown }; + if (!res2.ok || data2.error) { + return NextResponse.json( + { error: `Agent 2 failed: ${data2.error || 'unknown'}`, payment_details: data2.payment_details, partial: results }, + { status: res2.status } + ); + } + + const latency2 = Date.now() - start2; + results.push({ agentId: agent2Id, output: data2.output || '', latencyMs: latency2 }); + + // Publish A2A activity to Ably + await pushToAbly({ + eventType: 'agent_run', + agentId: `${agent1Id}→${agent2Id}`, + agentName: `Compose: ${agent1Id} → ${agent2Id}`, + ownerWallet: '', + callerWallet: walletAddress, + priceXlm: 0, + timestamp: new Date().toISOString(), + }); + + return NextResponse.json({ + correlationId, + steps: results, + finalOutput: data2.output || '', + totalLatencyMs: latency1 + latency2, + }); +} diff --git a/app/api/faucet/claim/route.ts b/app/api/faucet/claim/route.ts new file mode 100644 index 0000000..268c195 --- /dev/null +++ b/app/api/faucet/claim/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { Keypair, Networks, Asset, Memo, TransactionBuilder, Operation, Horizon } from 'stellar-sdk'; +import Ably from 'ably'; + +const FAUCET_MAX_CLAIMS = 3; +const FAUCET_AMOUNT_XLM = 5; // 5 XLM placeholder until AF$ token is on-chain (see contracts/af_token) +const HORIZON_URL = process.env.NEXT_PUBLIC_HORIZON_URL || 'https://horizon-testnet.stellar.org'; +const NETWORK_PASSPHRASE = Networks.TESTNET; + +async function pushFaucetActivity(wallet: string, amount: number): Promise { + const key = process.env.ABLY_API_KEY; + if (!key) return; + try { + const ably = new Ably.Rest({ key }); + await ably.channels.get('marketplace').publish('new_agent', { + eventType: 'new_agent', + agentId: 'faucet', + agentName: 'AF$ Faucet', + ownerWallet: wallet, + callerWallet: wallet, + priceXlm: amount, + timestamp: new Date().toISOString(), + }); + } catch { /* ignore */ } +} + +export async function POST(req: NextRequest) { + const body = await req.json().catch(() => ({})) as { walletAddress?: string }; + const { walletAddress } = body; + + if (!walletAddress || walletAddress.length < 56) { + return NextResponse.json({ error: 'Invalid wallet address' }, { status: 400 }); + } + + const faucetSecret = process.env.STELLAR_AGENT_SECRET; + if (!faucetSecret) { + return NextResponse.json({ error: 'Faucet not configured (STELLAR_AGENT_SECRET missing)' }, { status: 503 }); + } + + // Check & update claims in Supabase + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + let currentClaims = 0; + + if (supabaseUrl && supabaseKey) { + const { createClient } = await import('@supabase/supabase-js'); + const supabase = createClient(supabaseUrl, supabaseKey); + + const { data } = await supabase + .from('faucet_claims') + .select('claims_count') + .eq('wallet_address', walletAddress) + .single(); + + currentClaims = data?.claims_count ?? 0; + if (currentClaims >= FAUCET_MAX_CLAIMS) { + return NextResponse.json({ error: 'Faucet claim limit reached (max 3 claims per wallet)' }, { status: 429 }); + } + } + + // Send XLM via Stellar + try { + const keypair = Keypair.fromSecret(faucetSecret); + const server = new Horizon.Server(HORIZON_URL); + const account = await server.loadAccount(keypair.publicKey()); + + const tx = new TransactionBuilder(account, { fee: '100', networkPassphrase: NETWORK_PASSPHRASE }) + .addOperation(Operation.payment({ + destination: walletAddress, + asset: Asset.native(), + amount: FAUCET_AMOUNT_XLM.toFixed(7), + })) + .addMemo(Memo.text('AF$ Faucet Claim')) + .setTimeout(30) + .build(); + + tx.sign(keypair); + const result = await server.submitTransaction(tx); + const txHash = result.hash; + + // Update claims in Supabase + if (supabaseUrl && supabaseKey) { + const { createClient } = await import('@supabase/supabase-js'); + const supabase = createClient(supabaseUrl, supabaseKey); + await supabase.from('faucet_claims').upsert({ + wallet_address: walletAddress, + claims_count: currentClaims + 1, + last_claim_at: new Date().toISOString(), + total_received_xlm: (currentClaims + 1) * FAUCET_AMOUNT_XLM, + }, { onConflict: 'wallet_address' }); + } + + await pushFaucetActivity(walletAddress, FAUCET_AMOUNT_XLM); + + return NextResponse.json({ + txHash, + claimsRemaining: Math.max(0, FAUCET_MAX_CLAIMS - (currentClaims + 1)), + amountXlm: FAUCET_AMOUNT_XLM, + explorerUrl: `https://stellar.expert/explorer/testnet/tx/${txHash}`, + }); + } catch (err) { + console.error('[faucet/claim] Error:', err); + return NextResponse.json({ error: `Faucet transaction failed: ${String(err)}` }, { status: 500 }); + } +} diff --git a/app/api/faucet/claims/route.ts b/app/api/faucet/claims/route.ts new file mode 100644 index 0000000..9ecb431 --- /dev/null +++ b/app/api/faucet/claims/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const FAUCET_MAX_CLAIMS = 3; + +export async function GET(req: NextRequest) { + const wallet = req.nextUrl.searchParams.get('wallet'); + if (!wallet || wallet.length < 56) { + return NextResponse.json({ error: 'Invalid wallet address' }, { status: 400 }); + } + + // In a full implementation this would query Soroban contract state. + // For now we track claims in Supabase if available. + try { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (supabaseUrl && supabaseKey) { + const { createClient } = await import('@supabase/supabase-js'); + const supabase = createClient(supabaseUrl, supabaseKey); + const { data, error } = await supabase + .from('faucet_claims') + .select('claims_count') + .eq('wallet_address', wallet) + .single(); + + if (error && error.code !== 'PGRST116') { + console.warn('[faucet/claims] DB error:', error); + } + + const claimed = data?.claims_count ?? 0; + return NextResponse.json({ + claimsRemaining: Math.max(0, FAUCET_MAX_CLAIMS - claimed), + totalClaimed: claimed, + wallet, + }); + } + + return NextResponse.json({ + claimsRemaining: FAUCET_MAX_CLAIMS, + totalClaimed: 0, + wallet, + }); + } catch (err) { + return NextResponse.json({ error: String(err) }, { status: 500 }); + } +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index f108b44..0babe06 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -349,6 +349,59 @@ export default function DashboardPage() { ))} + {/* Platform Stats + QStash Widget */} +
+
+
+

Platform Stats

+ live metrics +
+
+ {[ + { label: 'GitHub Stars', value: '⭐ Live', color: 'text-[#f59e0b]', sub: 'mesayanroy/AgentForge' }, + { label: 'QStash Messages', value: String(process.env.NEXT_PUBLIC_QSTASH_MESSAGE_COUNT || '∞'), color: 'text-purple-400', sub: 'async delivery' }, + { label: 'AF$ Credits', value: '5,000', color: 'text-[#00FFE5]', sub: 'per faucet claim' }, + { label: 'Agents Live', value: String(myAgents.length || 0), color: 'text-[#4ade80]', sub: 'deployed globally' }, + ].map((s) => ( +
+
{s.value}
+
{s.label}
+
{s.sub}
+
+ ))} +
+
+ +
+
+

QStash Streaming

+ async delivery layer +
+
+ {[ + { topic: 'marketplace_activity', status: 'active', latency: '~120ms' }, + { topic: 'agent_run_request', status: 'active', latency: '~85ms' }, + { topic: 'a2a_request', status: 'active', latency: '~200ms' }, + { topic: 'payment_webhook', status: 'active', latency: '~95ms' }, + ].map((q) => ( +
+
+ + {q.topic} +
+
+ {q.status} + {q.latency} +
+
+ ))} +
+

+ QStash ensures reliable async delivery for all agent events via Upstash. +

+
+
+ {/* Charts Row 1: Request Rate + Billing by Model */}
diff --git a/app/faucet/page.tsx b/app/faucet/page.tsx new file mode 100644 index 0000000..32d5591 --- /dev/null +++ b/app/faucet/page.tsx @@ -0,0 +1,196 @@ +'use client'; + +import { useState } from 'react'; +import { motion } from 'framer-motion'; + +const AF_TOKEN_CONTRACT = process.env.NEXT_PUBLIC_AF_TOKEN_CONTRACT_ID || ''; +const FAUCET_AMOUNT = 5000; +const MAX_CLAIMS = 3; + +export default function FaucetPage() { + const [walletAddress, setWalletAddress] = useState(''); + const [claimsRemaining, setClaimsRemaining] = useState(null); + const [status, setStatus] = useState<'idle' | 'checking' | 'claiming' | 'success' | 'error'>('idle'); + const [txHash, setTxHash] = useState(null); + const [errorMsg, setErrorMsg] = useState(''); + const [totalClaimed, setTotalClaimed] = useState(0); + + async function checkClaims() { + if (!walletAddress.trim() || walletAddress.length < 56) return; + setStatus('checking'); + setErrorMsg(''); + try { + const res = await fetch(`/api/faucet/claims?wallet=${encodeURIComponent(walletAddress.trim())}`); + const data = await res.json() as { claimsRemaining: number; totalClaimed: number; error?: string }; + if (data.error) throw new Error(data.error); + setClaimsRemaining(data.claimsRemaining); + setTotalClaimed(data.totalClaimed || 0); + setStatus('idle'); + } catch (err) { + setErrorMsg(err instanceof Error ? err.message : 'Failed to check claims'); + setStatus('error'); + } + } + + async function claimTokens() { + if (!walletAddress.trim()) return; + setStatus('claiming'); + setErrorMsg(''); + try { + const res = await fetch('/api/faucet/claim', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ walletAddress: walletAddress.trim() }), + }); + const data = await res.json() as { txHash?: string; claimsRemaining?: number; error?: string }; + if (!res.ok || data.error) throw new Error(data.error || 'Claim failed'); + setTxHash(data.txHash || null); + setClaimsRemaining(data.claimsRemaining ?? null); + setStatus('success'); + } catch (err) { + setErrorMsg(err instanceof Error ? err.message : 'Claim failed'); + setStatus('error'); + } + } + + return ( +
+
+ {/* Header */} + +
+ AF$ Faucet + +
+

+ Claim AF$ Tokens +

+

+ Get {FAUCET_AMOUNT} AF$ tokens up to {MAX_CLAIMS}× for free. Use them to trade, + stake, and test agents on the platform. +

+
+ + {/* Stats */} +
+ {[ + { label: 'Per Claim', value: `${FAUCET_AMOUNT} AF$`, color: '#00FFE5' }, + { label: 'Max Claims', value: `${MAX_CLAIMS}×`, color: '#f59e0b' }, + { label: 'Total Supply', value: '100M AF$', color: '#4ade80' }, + ].map((s) => ( +
+
{s.value}
+
{s.label}
+
+ ))} +
+ + {/* Faucet Form */} + +
+ + { + setWalletAddress(e.target.value); + setClaimsRemaining(null); + setStatus('idle'); + setTxHash(null); + }} + onBlur={checkClaims} + placeholder="G... (56 character Stellar address)" + className="w-full bg-white/[0.04] border border-white/[0.08] rounded-lg px-4 py-3 text-white placeholder-white/30 font-mono text-sm focus:outline-none focus:border-[#00FFE5]/50 transition-colors" + /> +
+ + {/* Claims remaining */} + {claimsRemaining !== null && status !== 'error' && ( +
+ Claims remaining: + 0 ? 'text-[#00FFE5]' : 'text-red-400'}`}> + {claimsRemaining} / {MAX_CLAIMS} + + {totalClaimed > 0 && ( + + · Already received {totalClaimed * FAUCET_AMOUNT} AF$ + + )} +
+ )} + + {/* Action button */} + + + {/* Success */} + {status === 'success' && txHash && ( + +

🎉 {FAUCET_AMOUNT} AF$ sent to your wallet!

+ + {txHash} + +
+ )} + + {/* Error */} + {status === 'error' && errorMsg && ( +
+

{errorMsg}

+
+ )} +
+ + {/* Info */} +
+

AF$ tokens are for testnet use only.

+

Use them to run agents, trade on the playground, and test the 0x402 payment protocol.

+ {AF_TOKEN_CONTRACT && ( +

+ Contract: {AF_TOKEN_CONTRACT.slice(0, 16)}...{AF_TOKEN_CONTRACT.slice(-8)} +

+ )} +
+
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index e345b5b..4e28c03 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import "./globals.css"; import Navbar from "@/components/Navbar"; import AppShell from "@/components/AppShell"; +import AblyNotifications from "@/components/AblyNotifications"; export const metadata: Metadata = { title: "AgentForge — AI Agent Marketplace on Stellar", @@ -25,7 +26,9 @@ export default function RootLayout({
{children}
+ ); } + diff --git a/app/workflow/page.tsx b/app/workflow/page.tsx index f7e9250..14ab791 100644 --- a/app/workflow/page.tsx +++ b/app/workflow/page.tsx @@ -765,9 +765,16 @@ export default function WorkflowPage() { const [uploadedFile, setUploadedFile] = useState(null); const fileRef = useRef(null); + // Strategy Notes state + const [notes, setNotes] = useState(''); + const [notesSaved, setNotesSaved] = useState(false); + useEffect(() => { const addr = localStorage.getItem('wallet_address'); setWalletAddress(addr); + // Load persisted notes + const saved = localStorage.getItem('agentforge-workflow-notes'); + if (saved) setNotes(saved); }, []); // Canvas history helpers @@ -845,6 +852,25 @@ export default function WorkflowPage() { setTasks((t) => t.map((x) => x.status === 'done' ? x : { ...x, status: 'idle' })); } + function saveNotes() { + localStorage.setItem('agentforge-workflow-notes', notes); + setNotesSaved(true); + setTimeout(() => setNotesSaved(false), 2000); + } + + function sendNotesToTasks() { + const lines = notes.split('\n'); + const checkboxTasks = lines + .filter((line) => line.trim().startsWith('- [ ]')) + .map((line) => line.replace(/^- \[ \]\s*/, '').trim()) + .filter(Boolean); + if (checkboxTasks.length === 0) return; + setTasks((t) => [ + ...t, + ...checkboxTasks.map((label) => ({ id: uid(), label, status: 'idle' as const })), + ]); + } + if (!walletAddress) { return (
@@ -1086,6 +1112,44 @@ export default function WorkflowPage() { > + + {/* ── Strategy Notes ── */} + +
+
+

Strategy Notes

+

+ Saved to localStorage · Lines starting with - [ ] become tasks +

+
+
+ + +
+
+