diff --git a/app/devs/page.mdx b/app/devs/page.mdx index e6506ef..10bd802 100644 --- a/app/devs/page.mdx +++ b/app/devs/page.mdx @@ -1,98 +1,718 @@ -
-
+ + +
+ + + +
+ +
+ Home + / + Developer Hub +
+ +
Developer Hub
+
+ Production-first integration reference for building on the 0x402 protocol β€” real Stellar payments, QStash event routing, and AI agent orchestration. +
+ +
+
Getting Started
+
+ +
+🌐 What is 0x402 Protocol?+ +
+ +

The 0x402 Protocol is an open standard for machine-to-machine micropayments built on top of the HTTP specification. It extends the dormant 402 Payment Required status code into a fully functional request-payment-retry handshake, enabling any API endpoint to monetize its responses in real time.

+ +

In the AgentForge implementation, 0x402 is powered by the Stellar blockchain. When a client calls a paid agent endpoint and the server detects no valid payment credential, it responds with:

+ +
{`HTTP/1.1 402 Payment Required
+X-Payment-Required: xlm
+X-Payment-Amount: 0.05
+X-Payment-Address: GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+X-Payment-Network: stellar
+X-Payment-Memo: agent::req:`}
+ +

The client then creates and submits a real XLM transaction to the Stellar network, captures the transaction hash, and retries the original request with the payment proof in headers. The server verifies the transaction on-chain via Horizon before fulfilling the request.

+ +

This creates a trustless, permissionless payment layer where:

+
    +
  • No API keys or billing accounts are needed
  • +
  • Payments settle in 3–5 seconds on Stellar
  • +
  • The payment proof is permanently auditable on-chain
  • +
  • Any wallet can pay any agent without prior registration
  • +
+ +
πŸ’‘ Tip: The minimum denomination is 0.0000001 XLM (1 stroop), making sub-cent micropayments economically viable for high-frequency agent calls.
+ +
+
+ +
+πŸ” How do I connect my Freighter wallet?+ +
+ +

Freighter is a browser extension wallet for the Stellar network, analogous to MetaMask for Ethereum. It is the primary wallet integration in AgentForge for user-facing payment flows.

+ +

Installation:

+
    +
  1. Install the Freighter extension from freighter.app or the Chrome/Firefox Web Store
  2. +
  3. Create or import a Stellar account (12 or 24-word mnemonic)
  4. +
  5. Switch to Testnet in Freighter settings during development
  6. +
  7. Fund your testnet account using https://friendbot.stellar.org/?addr=YOUR_ADDRESS
  8. +
+ +

Integration in your app:

+ +
{`import { isConnected, getPublicKey, signTransaction } from '@stellar/freighter-api';
+
+// Check if Freighter is installed
+const connected = await isConnected();
+if (!connected) throw new Error('Freighter not installed');
+
+// Request the user's public key
+const publicKey = await getPublicKey();
+
+// Sign and submit a payment transaction
+const signedXDR = await signTransaction(unsignedXDR, {
+  network: 'TESTNET',
+  networkPassphrase: 'Test SDF Network ; September 2015',
+});`}
+ +

How AgentForge uses Freighter: When an agent endpoint returns a 402, the WalletConnect component triggers the payXLM helper which builds a Stellar payment transaction, presents it to Freighter for signing, and submits the signed XDR to Horizon. The resulting transaction hash is forwarded in the retry request header.

+ +
⚠️ Note: Freighter requires the user to approve each transaction. For non-interactive server-side flows (CLI or A2A), use a raw Stellar secret key passed via --secret flag.
+ +
+
+ +
+πŸ’³ How does the 402 Payment flow work?+ +
+ +

The full end-to-end payment flow for a paid agent call involves three phases: probe β†’ pay β†’ prove.

+ +

Phase 1 β€” Probe (initial request):

+
{`POST /api/agents/:id/run
+Content-Type: application/json
+
+{ "input": "Analyze this on-chain orderbook spread" }`}
+ +

If the agent requires payment, the server returns a 402 with payment headers. No computation is performed yet.

+ +

Phase 2 β€” Pay (wallet transaction):

+
{`// Using Freighter or a raw keypair:
+const txHash = await wallet.payXLM(
+  paymentAddress,  // from X-Payment-Address header
+  paymentAmount,   // from X-Payment-Amount header  
+  paymentMemo      // from X-Payment-Memo header (CRITICAL β€” links tx to request)
+);`}
+ +

The memo field is the most important part β€” it binds the on-chain transaction to a specific agent request nonce, preventing replay attacks.

+ +

Phase 3 β€” Prove (retry with proof):

+
{`POST /api/agents/:id/run
+Content-Type: application/json
+X-Payment-Tx-Hash: 4e7a2b9c...
+X-Payment-Wallet: GCALLER...
+
+{ "input": "Analyze this on-chain orderbook spread" }`}
+ +

The server fetches the transaction from Horizon, verifies:

+
    +
  • The transaction hash is valid and confirmed on the correct network
  • +
  • The destination address matches the agent owner's wallet
  • +
  • The amount is β‰₯ the required price
  • +
  • The memo matches the expected pattern agent:<id>:req:<nonce>
  • +
  • The transaction has not been used before (replay protection via Supabase)
  • +
+ +

On success, the agent runs, results are returned, and a QStash agentforge.payment.confirmed event is published for billing and marketplace tracking.

+ +
πŸ’‘ The entire flow is implemented in lib/paymentVerifier.ts and the agent route handler at app/api/agents/[id]/run/route.ts.
+ +
+
+ +
+
API Reference
+
+ +
+πŸ€– How do I create and deploy an agent?+ +
+ +

Agents are created via the AgentBuilder UI or directly via the POST /api/agents endpoint. Each agent is owned by a Stellar wallet address and stored in Supabase.

+ +

Create via API:

+
{`POST /api/agents
+Content-Type: application/json
+X-Wallet-Address: GOWNER...
+
+{
+  "name": "MEV Scanner",
+  "description": "Scans Stellar DEX for arbitrage opportunities",
+  "model": "gpt-4o",
+  "systemPrompt": "You are a DeFi analyst specializing in Stellar AMM pools...",
+  "price": "0.05",
+  "isPublic": true,
+  "tags": ["defi", "stellar", "arbitrage"]
+}`}
+ +

Response:

+
{`{
+  "id": "agt_7f3k2...",
+  "name": "MEV Scanner",
+  "owner": "GOWNER...",
+  "price": "0.05",
+  "model": "gpt-4o",
+  "isPublic": true,
+  "createdAt": "2024-01-15T10:23:00Z",
+  "runUrl": "/api/agents/agt_7f3k2.../run"
+}`}
+ +

Deploy checklist:

+
    +
  1. Ensure SUPABASE_SERVICE_ROLE_KEY and OPENAI_API_KEY (or your model provider key) are set
  2. +
  3. Create your agent via UI or API
  4. +
  5. Test the run endpoint β€” first call should return 402
  6. +
  7. Verify payment flow with a testnet wallet
  8. +
  9. Check dashboard analytics to confirm billing data populates
  10. +
  11. Set isPublic: true to list on the marketplace
  12. +
-# Developer Hub +
⚠️ Free agents: Set price: "0" to create a free agent. The 402 flow is skipped entirely β€” requests are served immediately without payment verification.
-Production-first integration reference for building on 0x402 + Stellar + QStash. +
+
+ +
+πŸ’° What is the minimum price per request?+ +
+ +

The minimum price for a paid agent request is 0.0000001 XLM (1 stroop), the smallest denomination on Stellar. In practice, meaningful pricing starts at around 0.01 XLM (~$0.001) per call.

+ +

Recommended pricing tiers:

+ + + + + + + + + + + +
Use CasePrice (XLM)Est. USD
Simple lookup / classification0.01 – 0.05$0.001 – $0.005
Standard LLM completion0.05 – 0.20$0.005 – $0.02
Complex analysis / long context0.20 – 1.00$0.02 – $0.10
Premium / specialized model1.00 – 5.00$0.10 – $0.50
+ +

Stellar transaction fees are fixed at 100 stroops (0.00001 XLM) per transaction β€” negligible compared to any reasonable pricing. There are no protocol-level platform fees on AgentForge; the full amount goes directly to the agent owner's wallet.

+ +
πŸ’‘ For agents that handle batched or long-running tasks, consider implementing your own rate limiting and bundling multiple agent calls into a single higher-value payment to amortize the per-transaction verification overhead.
+ +
+
+ +
+⛓️ How are payments verified on Stellar?+ +
-## JavaScript Client Pattern +

Payment verification is performed server-side by querying the Stellar Horizon API. The verifier checks five conditions to accept a payment as valid.

-```ts -async function runPaidAgent(agentId: string, input: string, wallet: { +

Verification logic (simplified):

+
{`async function verifyPayment(txHash: string, agentId: string, expectedAmount: string, ownerAddress: string) {
+  // 1. Fetch transaction from Horizon
+  const tx = await horizon.transactions().transaction(txHash).call();
+
+  // 2. Ensure it's confirmed (not pending)
+  if (!tx.successful) throw new Error('Transaction not confirmed');
+
+  // 3. Parse operations β€” find the payment op
+  const ops = await tx.operations().call();
+  const paymentOp = ops.records.find(op => op.type === 'payment');
+
+  // 4. Validate destination, amount, asset
+  if (paymentOp.to !== ownerAddress) throw new Error('Wrong destination');
+  if (parseFloat(paymentOp.amount) < parseFloat(expectedAmount)) throw new Error('Insufficient amount');
+  if (paymentOp.asset_type !== 'native') throw new Error('Non-XLM payment');
+
+  // 5. Verify memo matches request nonce (replay protection)
+  const expectedMemo = \`agent:\${agentId}:req:\${nonce}\`;
+  if (tx.memo !== expectedMemo) throw new Error('Memo mismatch');
+
+  // 6. Check not already used (idempotency)
+  const used = await db.requestLogs.findFirst({ where: { txHash } });
+  if (used) throw new Error('Transaction already redeemed');
+
+  return { verified: true, txHash, explorerUrl: \`https://stellar.expert/explorer/testnet/tx/\${txHash}\` };
+}`}
+ +

The Horizon endpoint is configured via NEXT_PUBLIC_HORIZON_URL. For testnet: https://horizon-testnet.stellar.org. For mainnet: https://horizon.stellar.org.

+ +

Explorer links are stored alongside each verified request in Supabase, surfaced in the dashboard invoice table and returned in the run API response body.

+ +
+
+ +
+πŸ”Œ How do I integrate the API in my app?+ +
+ +

The complete JavaScript/TypeScript integration pattern handles the full 402 handshake in a single reusable function:

+ +
{`// lib/agentClient.ts
+
+export interface AgentWallet {
   address: string;
   payXLM: (to: string, amount: string, memo: string) => Promise;
-}) {
-  const url = `/api/agents/${agentId}/run`;
+}
 
-  let res = await fetch(url, {
-    method: 'POST',
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify({ input }),
-  });
+export async function runAgent(
+  agentId: string,
+  input: string,
+  wallet?: AgentWallet
+): Promise<{ output: string; txHash?: string; explorerUrl?: string }> {
+  const url = \`/api/agents/\${agentId}/run\`;
+  const body = JSON.stringify({ input });
+  const baseHeaders = { 'Content-Type': 'application/json' };
 
+  // Initial probe request
+  let res = await fetch(url, { method: 'POST', headers: baseHeaders, body });
+
+  // Handle 402 payment challenge
   if (res.status === 402) {
-    const amount = res.headers.get('X-Payment-Amount') || '0';
-    const address = res.headers.get('X-Payment-Address') || '';
-    const memo = res.headers.get('X-Payment-Memo') || '';
+    if (!wallet) throw new Error('This agent requires payment β€” provide a wallet');
+
+    const amount  = res.headers.get('X-Payment-Amount')  ?? '0';
+    const address = res.headers.get('X-Payment-Address') ?? '';
+    const memo    = res.headers.get('X-Payment-Memo')    ?? '';
 
+    // Submit payment and get transaction hash
     const txHash = await wallet.payXLM(address, amount, memo);
 
+    // Retry with payment proof
     res = await fetch(url, {
       method: 'POST',
       headers: {
-        'Content-Type': 'application/json',
+        ...baseHeaders,
         'X-Payment-Tx-Hash': txHash,
-        'X-Payment-Wallet': wallet.address,
+        'X-Payment-Wallet':  wallet.address,
       },
-      body: JSON.stringify({ input }),
+      body,
     });
   }
 
+  if (!res.ok) {
+    const err = await res.json().catch(() => ({}));
+    throw new Error(err.error ?? \`Agent error: \${res.status}\`);
+  }
+
   return res.json();
-}
-```
+}`}
+ +

Usage with Freighter:

+
{`import { getPublicKey, signTransaction } from '@stellar/freighter-api';
+import { TransactionBuilder, Networks, Asset, Operation, Server } from '@stellar/stellar-sdk';
+
+const wallet: AgentWallet = {
+  address: await getPublicKey(),
+  payXLM: async (to, amount, memo) => {
+    const server = new Server(process.env.NEXT_PUBLIC_HORIZON_URL!);
+    const account = await server.loadAccount(wallet.address);
+    const tx = new TransactionBuilder(account, {
+      fee: '100',
+      networkPassphrase: Networks.TESTNET,
+    })
+      .addOperation(Operation.payment({ destination: to, asset: Asset.native(), amount }))
+      .addMemo(Memo.text(memo))
+      .setTimeout(30)
+      .build();
+
+    const signedXDR = await signTransaction(tx.toXDR(), { network: 'TESTNET' });
+    const result = await server.submitTransaction(TransactionBuilder.fromXDR(signedXDR, Networks.TESTNET));
+    return result.hash;
+  },
+};
+
+const result = await runAgent('agt_7f3k2...', 'Analyze this pool', wallet);
+console.log(result.output);`}
+ +
+
+ +
+
CLI & Tools
+
+ +
+⌨️ How do I use the CLI?+ +
-## CLI Commands +

The AgentForge CLI (cli/ directory) provides a terminal interface for all platform operations, including agent management, transaction inspection, and A2A routing β€” without needing the browser UI.

-```bash -# list active agents +

Agent commands:

+
{`# List all available agents
 npm run cli -- agents list
 
-# run with 0x402 payment flow from terminal
-npm run cli -- agents run  -i "Find arbitrage route" --secret 
+# Run an agent with automatic 402 payment handling
+# --secret accepts a raw Stellar secret key for non-interactive payment
+npm run cli -- agents run  \
+  -i "Find arbitrage route on XLM/USDC" \
+  --secret SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
 
-# check ledger confirmation
+# Create a new agent
+npm run cli -- agents create \
+  --name "My Agent" \
+  --model gpt-4o \
+  --price 0.05 \
+  --system "You are a helpful assistant"
+
+# Inspect an agent's details
+npm run cli -- agents get `}
+ +

Transaction commands:

+
{`# Check if a transaction is confirmed on Stellar
 npm run cli -- tx status 
 
-# inspect transaction details
+# Inspect full transaction details (ops, memo, fees, timing)
 npm run cli -- tx inspect 
 
-# a2a routed call
-npm run cli -- a2a call   -i "Delegate this task"
-```
+# List recent transactions for a wallet
+npm run cli -- tx history --wallet GCALLER...`}
-## Required Runtime Variables +

A2A commands:

+
{`# Route a call from one agent to another
+npm run cli -- a2a call   \
+  -i "Delegate: summarize this on-chain report" \
+  --secret SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
 
-| Key | Purpose |
-| --- | --- |
-| NEXT_PUBLIC_HORIZON_URL | Horizon endpoint for tx verification |
-| NEXT_PUBLIC_STELLAR_NETWORK | testnet or mainnet explorer mode |
-| QSTASH_URL | Upstash region endpoint |
-| QSTASH_TOKEN | QStash publish auth |
-| QSTASH_CURRENT_SIGNING_KEY | webhook signature verification |
-| QSTASH_NEXT_SIGNING_KEY | key rotation support |
-| SUPABASE_SERVICE_ROLE_KEY | write request logs and billing |
+# View A2A event log
+npm run cli -- a2a log`}
-## Real-Time Observability +

The CLI reads NEXT_PUBLIC_HORIZON_URL and NEXT_PUBLIC_STELLAR_NETWORK from the environment, so make sure a .env.local file is present or variables are exported before running commands.

-- Dashboard polls /api/dashboard/analytics every 5 seconds. -- Live feed uses Ably marketplace channel. -- Invoice stream links each signature to Stellar explorer. -- No static mocks are used in analytics/invoice charts. +
+
-## A2A Payment Notes +
+πŸ“¨ What are QStash topics and consumers?+ +
-- Caller agent should provide X-Payment-Wallet identity. -- If target agent is paid, include tx hash generated by caller wallet. -- A2A events are delivered through QStash topics and consumer webhooks. +

QStash (by Upstash) is the event backbone of AgentForge. It provides durable, exactly-once message delivery via HTTP webhooks with built-in request signing for security. Every significant platform event is published to a QStash topic and delivered to one or more consumer endpoints.

-## Deployment Checklist +

Topic map:

+ + + + + + + + + + + + +
TopicTriggered by
agentforge.payment.pending402 challenge issued to client
agentforge.payment.confirmedOn-chain tx verified by Horizon
agentforge.agent.completedAgent run finished (paid or free)
agentforge.billing.updatedEarnings aggregated for owner wallet
agentforge.marketplace.activityPublic agent interactions (live feed)
agentforge.chain.syncedPeriodic Horizon sync checkpoint
agentforge.a2a.requestA2A routing request dispatched
agentforge.a2a.responseA2A target agent response received
-1. Configure Supabase URL + service role key. -2. Configure QStash URL/token/signing keys. -3. Set Stellar network variables. -4. Ensure ABLY_API_KEY is set for live activity stream. -5. Run consumers and verify /api/consumers/* receives signed QStash payloads. +

Consumer endpoints (app/api/consumers/) receive QStash deliveries, verify the Upstash-Signature header using QSTASH_CURRENT_SIGNING_KEY / QSTASH_NEXT_SIGNING_KEY, then update Supabase records and broadcast to Ably channels.

-
+
{`// Signature verification pattern (all consumer endpoints)
+import { Receiver } from '@upstash/qstash';
+
+const receiver = new Receiver({
+  currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
+  nextSigningKey:    process.env.QSTASH_NEXT_SIGNING_KEY!,
+});
+
+await receiver.verify({
+  signature: req.headers.get('Upstash-Signature') ?? '',
+  body:      rawBody,
+});`}
+ +
⚠️ Note: Consumer endpoints must be publicly reachable for QStash delivery. In local development, use ngrok or cloudflared tunnel to expose localhost:3000.
+ +
+ + +
+πŸ“Š How do I monitor my agent's performance?+ +
+ +

AgentForge provides real-time observability through two surfaces: the Dashboard (browser) and the Analytics API (programmatic).

+ +

Dashboard features:

+
    +
  • Candlestick chart of request volume (5-second polling via /api/dashboard/analytics)
  • +
  • Earnings breakdown by model and time window
  • +
  • Invoice table with Stellar Explorer links for every paid request
  • +
  • Live activity feed via Ably WebSocket channel (agentforge-live)
  • +
  • Per-agent request history with latency, status, and tx hash
  • +
+ +

Analytics API:

+
{`GET /api/dashboard/analytics?owner=GWALLET...&hours=24
+
+// Response shape:
+{
+  "byModel": [
+    { "model": "gpt-4o", "paidCount": 142, "freeCount": 38, "earnings": "7.10" }
+  ],
+  "requestRate": [
+    { "minute": "2024-01-15T10:00:00Z", "count": 12 }
+  ],
+  "earnings": [
+    { "date": "2024-01-15", "total": "7.10" }
+  ],
+  "invoices": [
+    {
+      "txHash": "4e7a2b9c...",
+      "amount": "0.05",
+      "agentId": "agt_7f3k2...",
+      "timestamp": "2024-01-15T10:23:04Z",
+      "explorerUrl": "https://stellar.expert/explorer/testnet/tx/4e7..."
+    }
+  ]
+}`}
+ +

All analytics data is sourced from live Supabase records β€” no static mocks. The hours parameter accepts 1, 6, 24, 72, or 168 (7 days).

+ +
+
+ +
+
Advanced
+
+ +
+🧠 What AI models are supported?+ +
+ +

AgentForge supports any model exposed via an OpenAI-compatible API. The model is specified per-agent and passed through to the completion provider on each run.

+ +

Supported providers and models:

+ + + + + + + + + +
ProviderModelsEnv Key Required
OpenAIgpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turboOPENAI_API_KEY
Anthropicclaude-3-5-sonnet, claude-3-haikuANTHROPIC_API_KEY
Googlegemini-1.5-pro, gemini-1.5-flashGOOGLE_API_KEY
Groqllama-3.1-70b, mixtral-8x7bGROQ_API_KEY
Local / CustomAny OpenAI-compatible endpointCUSTOM_LLM_BASE_URL
+ +

The model selection is stored in the agent record and resolved at run time. Dashboard analytics break down earnings and request counts per model, so you can track the cost and revenue profile of each model tier you offer.

+ +
πŸ’‘ For cost-optimized agents, use gpt-4o-mini or gemini-1.5-flash at lower price points. For premium agents requiring complex reasoning, use gpt-4o or claude-3-5-sonnet at higher price points to maintain margin.
+ +
+
+ +
+πŸ”€ How does A2A (Agent-to-Agent) routing work?+ +
+ +

Agent-to-Agent (A2A) routing enables one agent to call another as part of its execution β€” effectively creating multi-hop AI pipelines where each hop may have its own payment requirement.

+ +

A2A flow:

+
    +
  1. Agent A receives a request that requires sub-task delegation
  2. +
  3. Agent A calls POST /api/a2a/route with the target agent ID and sub-task input
  4. +
  5. If the target agent (B) is paid, Agent A's wallet pays Agent B's owner
  6. +
  7. The A2A request is published to agentforge.a2a.request QStash topic
  8. +
  9. The consumer delivers the request to Agent B's run endpoint
  10. +
  11. Agent B's response is published to agentforge.a2a.response
  12. +
  13. Agent A's execution resumes with the delegated result
  14. +
+ +

A2A API:

+
{`POST /api/a2a/route
+Content-Type: application/json
+X-Payment-Wallet: GCALLER_AGENT...
+X-Payment-Tx-Hash: 
+
+{
+  "fromAgentId": "agt_caller...",
+  "toAgentId":   "agt_target...",
+  "input":       "Summarize the MEV opportunity found in this orderbook data: ..."
+}`}
+ +

CLI equivalent:

+
{`npm run cli -- a2a call agt_caller... agt_target... \
+  -i "Delegate: analyze this pool liquidity" \
+  --secret SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`}
+ +

A2A identity: The X-Payment-Wallet header in an A2A call identifies the calling agent's owner wallet. This creates an on-chain audit trail linking caller β†’ callee wallet addresses, allowing billing to be correctly attributed across the agent graph.

+ +
⚠️ Circular routing prevention: A2A calls track the call chain depth. Chains deeper than 5 hops are rejected to prevent infinite delegation loops. This limit is configurable via the A2A_MAX_DEPTH environment variable.
+ +
+
+ +
+
Environment Variables
+ + + + + + + + + + + + + +
VariablePurpose
NEXT_PUBLIC_HORIZON_URLHorizon endpoint for tx verification
NEXT_PUBLIC_STELLAR_NETWORKtestnet or mainnet
QSTASH_URLUpstash regional endpoint
QSTASH_TOKENQStash publish auth token
QSTASH_CURRENT_SIGNING_KEYWebhook signature verification
QSTASH_NEXT_SIGNING_KEYKey rotation support
SUPABASE_SERVICE_ROLE_KEYWrite request logs and billing records
ABLY_API_KEYLive activity broadcast to dashboard
OPENAI_API_KEYDefault LLM provider
+
+ +
diff --git a/app/docs/page.mdx b/app/docs/page.mdx index 76f23ef..e0a7124 100644 --- a/app/docs/page.mdx +++ b/app/docs/page.mdx @@ -1,120 +1,388 @@ import PaymentWorkflowDiagram from '@/components/docs/PaymentWorkflowDiagram'; import BackboneDiagram from '@/components/docs/BackboneDiagram'; -# 0x402 Protocol Docs + -Build paid AI-to-AI and user-to-agent requests with real Stellar transactions, Freighter signatures, and a QStash real-time event backbone. +
+ + + +
+ +
+
Protocol Documentation
+
0x402 Protocol Docs
+
+ Build paid AI-to-AI and user-to-agent requests with real Stellar transactions, Freighter signatures, and a QStash real-time event backbone. +
+
+ +
+
Architecture Overview
+
-## End-to-End Flow +
+ AgentForge is built on three interlocking primitives: the 0x402 payment challenge (HTTP-layer micropayments), the Stellar ledger (settlement and auditability), and QStash (durable event delivery). Every paid agent interaction touches all three layers. +
+ +
+
End-to-End Payment Flow
+
-1. Client calls POST /api/agents/:id/run. -2. If paid, API returns HTTP 402 with X-Payment-* headers. -3. Client signs/submits an XLM tx with Freighter wallet. -4. Client retries run request with X-Payment-Tx-Hash and X-Payment-Wallet. -5. Platform verifies tx against Horizon and stores signature in request logs. -6. QStash events push billing and marketplace activity in real time. -7. Dashboard charts + invoice table update with explorer links. +
    +
  1. Client calls POST /api/agents/:id/run with the task input
  2. +
  3. If the agent is paid, the API returns HTTP 402 with X-Payment-* headers specifying amount, address, and memo
  4. +
  5. Client signs and submits an XLM payment transaction via Freighter wallet (browser) or raw keypair (CLI/A2A)
  6. +
  7. Client retries the run request with X-Payment-Tx-Hash and X-Payment-Wallet headers
  8. +
  9. Platform verifies the transaction against Stellar Horizon β€” checks destination, amount, memo nonce, and replay protection
  10. +
  11. Agent executes β€” LLM completion runs, result is returned with tx_hash and tx_explorer_url in the response body
  12. +
  13. QStash events push billing and marketplace activity in real time to dashboard and Ably live feed
  14. +
-## API Contracts +
+
API Contracts
+
-### Run an Agent +
+ + + POST + /api/agents/:id/run + + Run an Agent + +
-```http -POST /api/agents/:id/run +
Execute an agent by ID. Returns a 402 challenge if the agent requires payment.
+ +
+
Request
+
{`POST /api/agents/:id/run
 Content-Type: application/json
 
 {
   "input": "Analyze this on-chain orderbook spread"
-}
-```
-
-402 challenge response:
+}`}
+
-```http -HTTP/1.1 402 Payment Required +
+
402 Payment Challenge
+
{`HTTP/1.1 402 Payment Required
 X-Payment-Required: xlm
 X-Payment-Amount: 0.05
 X-Payment-Address: G...OWNER
 X-Payment-Network: stellar
-X-Payment-Memo: agent::req:
-```
-
-Paid retry:
+X-Payment-Memo: agent::req:`}
+
-```http -POST /api/agents/:id/run +
+
Paid Retry
+
{`POST /api/agents/:id/run
 Content-Type: application/json
 X-Payment-Tx-Hash: 
-X-Payment-Wallet: 
+X-Payment-Wallet: 
 
 {
   "input": "Analyze this on-chain orderbook spread"
-}
-```
+}`}
+
+ +
+
Success Response (200)
+
{`{
+  "output": "Based on the current XLM/USDC spread of 0.23%...",
+  "request_id": "req_9a3f2b1c...",
+  "latency_ms": 238,
+  "tx_hash": "4e7a2b9c...",
+  "tx_explorer_url": "https://stellar.expert/explorer/testnet/tx/4e7a2b9c..."
+}`}
+
+ +
+
+ +
+ + + GET + /api/dashboard/analytics + + Dashboard Analytics + +
+ +
Returns real-time analytics for an agent owner's wallet. All data is sourced from live Supabase records β€” no mocks.
+ +
+
Request
+
{`GET /api/dashboard/analytics?owner=&hours=24`}
+
+ +
+
Response
+
{`{
+  "byModel": [
+    { "model": "gpt-4o", "paidCount": 142, "freeCount": 38, "earnings": "7.10" }
+  ],
+  "requestRate": [
+    { "minute": "2024-01-15T10:00:00Z", "count": 12 }
+  ],
+  "earnings": [
+    { "date": "2024-01-15", "total": "7.10" }
+  ],
+  "invoices": [
+    {
+      "txHash": "4e7a2b9c...",
+      "amount": "0.05",
+      "agentId": "agt_7f3k2...",
+      "callerWallet": "GCALLER...",
+      "timestamp": "2024-01-15T10:23:04Z",
+      "explorerUrl": "https://stellar.expert/explorer/testnet/tx/4e7a2b9c..."
+    }
+  ]
+}`}
+
-Successful response includes tx metadata: +
The hours parameter accepts: 1, 6, 24, 72, 168. Dashboard polls this endpoint every 5 seconds.
+ +
+
+ +
+ + + POST + /api/a2a/route + + Agent-to-Agent Route + +
+ +
Route a task from one agent to another. If the target agent is paid, include a valid X-Payment-Tx-Hash signed by the calling agent's wallet.
+ +
+
Request
+
{`POST /api/a2a/route
+Content-Type: application/json
+X-Payment-Wallet: GCALLER_AGENT...
+X-Payment-Tx-Hash: 
 
-```json
 {
-  "output": "...",
-  "request_id": "uuid",
-  "latency_ms": 238,
-  "tx_hash": "4e7...",
-  "tx_explorer_url": "https://stellar.expert/explorer/testnet/tx/4e7..."
-}
-```
+  "fromAgentId": "agt_caller...",
+  "toAgentId":   "agt_target...",
+  "input":       "Delegate: summarize this liquidity report"
+}`}
+
+ +
πŸ’‘ A2A chains are limited to 5 hops by default to prevent infinite delegation loops. Override with A2A_MAX_DEPTH env variable.
+ +
+
-### Dashboard Analytics +
+
QStash Topic Map
+
-```http -GET /api/dashboard/analytics?owner=&hours=24 -``` +
Every significant platform event is published to a QStash topic and delivered to one or more consumer webhook endpoints at app/api/consumers/.
-Returns: + + + + + + + + + + + + +
TopicTriggered by
agentforge.payment.pending402 challenge issued to client
agentforge.payment.confirmedOn-chain transaction verified via Horizon
agentforge.agent.completedAgent run finished (paid or free)
agentforge.billing.updatedEarnings aggregated for owner wallet
agentforge.marketplace.activityPublic agent interaction (live feed)
agentforge.chain.syncedPeriodic Horizon sync checkpoint
agentforge.a2a.requestA2A routing request dispatched
agentforge.a2a.responseA2A target agent response received
-- byModel: paid and free request stats per model. -- requestRate: minute buckets for real-time graphs. -- earnings: daily totals for billing charts. -- invoices: unique tx signatures + explorer URLs. +
⚠️ Consumer endpoints verify the Upstash-Signature header on every delivery using QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY for key rotation support.
-## CLI Quickstart +
+
CLI Quickstart
+
-```bash +
+
bash
+
{`# List all agents on the marketplace
 npm run cli -- agents list
-npm run cli -- agents run  -i "Summarize MEV opportunities" --secret 
+
+# Run a paid agent (handles 402 flow automatically with --secret)
+npm run cli -- agents run  \
+  -i "Summarize MEV opportunities in XLM/USDC pool" \
+  --secret SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+# Check transaction confirmation status
 npm run cli -- tx status 
-```
 
-## QStash Topic Map
+# Inspect full transaction details
+npm run cli -- tx inspect 
 
-- agentforge.payment.pending
-- agentforge.payment.confirmed
-- agentforge.agent.completed
-- agentforge.billing.updated
-- agentforge.marketplace.activity
-- agentforge.chain.synced
-- agentforge.a2a.request
-- agentforge.a2a.response
+# A2A routed call
+npm run cli -- a2a call   \
+  -i "Delegate: analyze this orderbook" \
+  --secret SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`}
+
-## Environment +
+
Explorer Verification
+
-```bash +
Every paid request stores the Stellar transaction hash and a direct Stellar Expert explorer URL in the Supabase agent_requests table. These are surfaced in:
+ +
    +
  • Dashboard invoice table β€” each row links to the on-chain tx page
  • +
  • Agent detail page β€” run history with tx hash column
  • +
  • Run API response body β€” tx_hash and tx_explorer_url fields
  • +
  • Billing calculations β€” derived exclusively from confirmed paid requests
  • +
+ +
+
Environment Variables
+
+ +
+
.env.local
+
{`# Stellar / Horizon
+NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org
+NEXT_PUBLIC_STELLAR_NETWORK=testnet
+
+# QStash (Upstash)
 QSTASH_URL=https://qstash-eu-central-1.upstash.io
 QSTASH_TOKEN=...
 QSTASH_CURRENT_SIGNING_KEY=...
 QSTASH_NEXT_SIGNING_KEY=...
-NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org
-NEXT_PUBLIC_STELLAR_NETWORK=testnet
-```
 
-## Explorer Verification
+# Supabase
+NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
+NEXT_PUBLIC_SUPABASE_ANON_KEY=...
+SUPABASE_SERVICE_ROLE_KEY=...
+
+# LLM Provider
+OPENAI_API_KEY=...
 
-Each paid request stores the tx signature and explorer URL in agent request logs.
+# Ably (live feed)
+ABLY_API_KEY=...`}
+
-- Dashboard invoice row links to Stellar Expert tx page. -- Agent detail and run API return tx_hash and tx_explorer_url. -- Billing is derived from confirmed paid requests, not mock data. +
+
diff --git a/app/trading/page.tsx b/app/trading/page.tsx index 45a8fec..e261322 100644 --- a/app/trading/page.tsx +++ b/app/trading/page.tsx @@ -10,7 +10,8 @@ import { XAxis, YAxis, } from 'recharts'; -import CandlestickChart from '@/components/CandlestickChart'; +import CandlestickChart, { CloseEvent } from '@/components/CandlestickChart'; +import type { Agent } from '@/types'; interface OHLC { ts: string; @@ -137,13 +138,24 @@ export default function TradingPage() { const [orders, setOrders] = useState([]); const [position, setPosition] = useState(null); const [closedPnl, setClosedPnl] = useState<{ pnl: number; pair: string; ts: string } | null>(null); + const [closeEvent, setCloseEvent] = useState(null); const [activeTab, setActiveTab] = useState<'chart' | 'agents'>('chart'); const [selectedAgent, setSelectedAgent] = useState(null); const [agentCategory, setAgentCategory] = useState('all'); + const [agentSubTab, setAgentSubTab] = useState<'templates' | 'mine' | 'arb-demo'>('templates'); + const [myDeployedAgents, setMyDeployedAgents] = useState([]); + const [loadingMyAgents, setLoadingMyAgents] = useState(false); const [orderError, setOrderError] = useState(null); const [orderSuccess, setOrderSuccess] = useState(null); const [walletBalance, setWalletBalance] = useState(null); const [walletAddress, setWalletAddress] = useState(null); + // Arbitrage demo state + const [arbRunning, setArbRunning] = useState(false); + const [arbStep, setArbStep] = useState(0); + const [arbPrices, setArbPrices] = useState({ binance: 0, coinbase: 0 }); + const [arbResult, setArbResult] = useState<{ profit: number; pair: string } | null>(null); + const [sigModalOpen, setSigModalOpen] = useState(false); + const [pendingArbTrade, setPendingArbTrade] = useState<{ buyEx: string; sellEx: string; spread: number; spreadPct: number } | null>(null); const tickerRef = useRef | null>(null); const priceIntervalRef = useRef | null>(null); @@ -210,6 +222,7 @@ export default function TradingPage() { const base = FALLBACK_PRICES[selectedPair.coinGeckoId] || 1; setCandles(generateHistory(base, 60)); setPosition(null); + setCloseEvent(null); }, [selectedPairId, selectedPair.coinGeckoId]); // Live price tick simulation β€” restart whenever candles are seeded or pair changes @@ -232,7 +245,7 @@ export default function TradingPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [candles.length, selectedPairId]); - // Load wallet balance + // Load wallet balance and user's deployed agents useEffect(() => { const addr = localStorage.getItem('wallet_address'); if (!addr) return; @@ -245,6 +258,26 @@ export default function TradingPage() { } catch { /* ignore */ } }; void load(); + + // Fetch user's own deployed agents + setLoadingMyAgents(true); + fetch(`/api/agents/list?owner=${encodeURIComponent(addr)}`) + .then((r) => r.ok ? r.json() : { agents: [] }) + .then((d: { agents?: Agent[] }) => setMyDeployedAgents(d.agents ?? [])) + .catch(() => setMyDeployedAgents([])) + .finally(() => setLoadingMyAgents(false)); + }, []); + + // Persist closed trade to localStorage so dashboard can sync immediately + const persistClosedTrade = useCallback((pnl: number, pair: string, closeType: 'tp' | 'sl' | 'manual', closePx: number) => { + try { + const existing: Array<{ pnl: number; pair: string; ts: string; type: string; closePrice: number }> = + JSON.parse(localStorage.getItem('trading_pnl_history') ?? '[]'); + existing.unshift({ pnl, pair, ts: new Date().toISOString(), type: closeType, closePrice: closePx }); + localStorage.setItem('trading_pnl_history', JSON.stringify(existing.slice(0, 50))); + // Dispatch custom event so dashboard (same tab) can update without polling delay + window.dispatchEvent(new CustomEvent('trading_pnl_update', { detail: { pnl, pair, type: closeType } })); + } catch { /* ignore */ } }, []); const recentHigh = candles.length ? Math.max(...candles.slice(-20).map((c) => c.high)) : 0; @@ -269,34 +302,42 @@ export default function TradingPage() { if (pos.tp && pos.side === 'long' && livePrice >= pos.tp) { const tpPnl = (pos.tp - pos.entryPrice) * pos.size * pos.leverage; setClosedPnl({ pnl: tpPnl, pair: pos.pair, ts: new Date().toISOString() }); + setCloseEvent({ price: pos.tp, type: 'tp', pnl: tpPnl }); setOrderSuccess(`🎯 Take Profit hit! PnL: +$${fmtPrice(tpPnl)} Β· ${pos.pair.toUpperCase()}`); + persistClosedTrade(tpPnl, pos.pair, 'tp', pos.tp); setPosition(null); return; } if (pos.tp && pos.side === 'short' && livePrice <= pos.tp) { const tpPnl = (pos.entryPrice - pos.tp) * pos.size * pos.leverage; setClosedPnl({ pnl: tpPnl, pair: pos.pair, ts: new Date().toISOString() }); + setCloseEvent({ price: pos.tp, type: 'tp', pnl: tpPnl }); setOrderSuccess(`🎯 Take Profit hit! PnL: +$${fmtPrice(tpPnl)} Β· ${pos.pair.toUpperCase()}`); + persistClosedTrade(tpPnl, pos.pair, 'tp', pos.tp); setPosition(null); return; } if (pos.sl && pos.side === 'long' && livePrice <= pos.sl) { const slPnl = (pos.sl - pos.entryPrice) * pos.size * pos.leverage; setClosedPnl({ pnl: slPnl, pair: pos.pair, ts: new Date().toISOString() }); + setCloseEvent({ price: pos.sl, type: 'sl', pnl: slPnl }); setOrderSuccess(`πŸ›‘ Stop Loss triggered. PnL: ${slPnl >= 0 ? '+' : ''}$${fmtPrice(slPnl)} Β· ${pos.pair.toUpperCase()}`); + persistClosedTrade(slPnl, pos.pair, 'sl', pos.sl); setPosition(null); return; } if (pos.sl && pos.side === 'short' && livePrice >= pos.sl) { const slPnl = (pos.entryPrice - pos.sl) * pos.size * pos.leverage; setClosedPnl({ pnl: slPnl, pair: pos.pair, ts: new Date().toISOString() }); + setCloseEvent({ price: pos.sl, type: 'sl', pnl: slPnl }); setOrderSuccess(`πŸ›‘ Stop Loss triggered. PnL: ${slPnl >= 0 ? '+' : ''}$${fmtPrice(slPnl)} Β· ${pos.pair.toUpperCase()}`); + persistClosedTrade(slPnl, pos.pair, 'sl', pos.sl); setPosition(null); return; } setPosition((prev) => prev ? { ...prev, unrealisedPnl: pnl } : null); - }, [candles]); + }, [candles, persistClosedTrade]); const submitOrder = useCallback(() => { setOrderError(null); setOrderSuccess(null); @@ -323,8 +364,11 @@ export default function TradingPage() { const closePosition = () => { if (!position) return; const pnl = position.unrealisedPnl; + const livePrice = candles[candles.length - 1]?.close ?? position.entryPrice; setClosedPnl({ pnl, pair: position.pair, ts: new Date().toISOString() }); + setCloseEvent({ price: livePrice, type: 'manual', pnl }); setOrderSuccess(`Position closed. PnL: ${pnl >= 0 ? '+' : ''}$${fmtPrice(pnl)} Β· ${position.pair.toUpperCase()}`); + persistClosedTrade(pnl, position.pair, 'manual', livePrice); setPosition(null); }; @@ -435,6 +479,7 @@ export default function TradingPage() { slLevel={position?.sl ?? null} liqLevel={position?.liquidationPrice ?? null} entryLevel={position?.entryPrice ?? null} + closeEvent={closeEvent} />
@@ -586,41 +631,58 @@ export default function TradingPage() { )} {activeTab === 'agents' && ( - -
-

Select an agent template to automate your trading strategy. Each call is metered via the 0x402 protocol.

-
- {['all', 'strategy', 'arbitrage', 'mev', 'monitor'].map((cat) => ( - - ))} -
-
-
- {filteredAgents.map((agent) => ( - -
-
-

{agent.name}

- {agent.category} -
-

{agent.description}

-
-
- {agent.tags.map((t) => #{t})} -
-
- {agent.priceXlm} XLM/req - -
-
+ + {/* Agent sub-tabs */} +
+ {([ + { id: 'templates', label: 'πŸ“¦ Agent Templates' }, + { id: 'mine', label: 'πŸš€ My Deployed Agents' }, + { id: 'arb-demo', label: '⚑ Arbitrage Demo' }, + ] as const).map((st) => ( + ))}
-
-

SDK Quick-Start (0x402)

-
{`// npm install @stellar/stellar-sdk ably
+
+              {/* Templates sub-tab */}
+              {agentSubTab === 'templates' && (
+                
+
+

Select an agent template to automate your trading strategy. Each call is metered via the 0x402 protocol.

+
+ {['all', 'strategy', 'arbitrage', 'mev', 'monitor'].map((cat) => ( + + ))} +
+
+
+ {filteredAgents.map((agent) => ( + +
+
+

{agent.name}

+ {agent.category} +
+

{agent.description}

+
+
+ {agent.tags.map((t) => #{t})} +
+
+ {agent.priceXlm} XLM/req + +
+
+ ))} +
+
+

SDK Quick-Start (0x402)

+
{`// npm install @stellar/stellar-sdk ably
 const agentId = '${selectedAgent ?? AGENT_TEMPLATES[0].id}';
 const res = await fetch(\`https://agentforge.dev/api/agents/\${agentId}/run\`, {
   method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -636,7 +698,261 @@ if (res.status === 402) {
   });
   console.log((await paid.json()).output);
 }`}
-
+
+
+ )} + + {/* My Deployed Agents sub-tab */} + {agentSubTab === 'mine' && ( +
+

+ These are agents you have deployed on AgentForge. Select one to trade with it on the mainnet β€” every trade order will be routed through your agent endpoint. +

+ {!walletAddress ? ( +
+ Connect your wallet to see your deployed agents. +
+ ) : loadingMyAgents ? ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ ) : myDeployedAgents.length === 0 ? ( +
+ No deployed agents found. Deploy one from the Agents page to get started. +
+ ) : ( +
+ {myDeployedAgents.map((agent) => ( + { setSelectedAgent(agent.id); setActiveTab('chart'); }}> +
+

{agent.name}

+ + {network === 'mainnet' ? '🌐 Mainnet' : 'πŸ”΅ Testnet'} + +
+

{agent.description ?? 'Custom deployed agent'}

+
+ {agent.id.slice(0, 12)}… + + {selectedAgent === agent.id ? 'βœ“ Selected' : 'Click to select'} + +
+
+ ))} +
+ )} +
+ )} + + {/* Arbitrage Demo sub-tab */} + {agentSubTab === 'arb-demo' && ( +
+
+

⚑ Arbitrage Agent β€” Live Demo

+

+ Watch how the Arbitrage Sentinel scans two exchanges, detects a price spread, asks for your wallet signature, and executes the cross-exchange trade β€” all in real time. +

+ + {/* Step indicators */} +
+ {['Scan exchanges', 'Detect spread', 'Review trade', 'Sign & execute', 'Result'].map((label, idx) => ( +
+
idx ? 'bg-[#4ade80] text-black' : arbStep === idx ? 'bg-[#FFB800] text-black animate-pulse' : 'bg-white/[0.05] text-gray-600'}`}> + {arbStep > idx ? 'βœ“' : idx + 1} +
+ + {idx < 4 && β€Ί} +
+ ))} +
+ + {/* Step 0: Start */} + {arbStep === 0 && ( +
+
+ {[{ name: 'Binance', logo: '🟑' }, { name: 'Coinbase', logo: 'πŸ”΅' }].map((ex) => ( +
+
{ex.logo} {ex.name}
+
Price: scanning…
+
+ ))} +
+ +
+ )} + + {/* Step 1: Scanning */} + {arbStep === 1 && ( +
+
+ {[{ name: 'Binance', logo: '🟑', price: arbPrices.binance }, { name: 'Coinbase', logo: 'πŸ”΅', price: arbPrices.coinbase }].map((ex) => ( +
+
{ex.logo} {ex.name}
+
Fetching… ⏳
+
+ ))} +
+
Agent is scanning live order books…
+
+ )} + + {/* Step 2: Spread detected */} + {arbStep === 2 && pendingArbTrade && ( +
+
+ {[{ name: 'Binance', logo: '🟑', price: arbPrices.binance }, { name: 'Coinbase', logo: 'πŸ”΅', price: arbPrices.coinbase }].map((ex) => ( +
+
{ex.logo} {ex.name}
+
${fmtPrice(ex.price)}
+
+ {ex.name === pendingArbTrade.buyEx ? '← BUY HERE' : 'β†’ SELL HERE'} +
+
+ ))} +
+
+
πŸ“Š Spread Detected
+
+
Buy on: {pendingArbTrade.buyEx}
+
Sell on: {pendingArbTrade.sellEx}
+
Spread: ${fmtPrice(pendingArbTrade.spread)}
+
Spread %: {pendingArbTrade.spreadPct.toFixed(3)}%
+
+
+ +
+ )} + + {/* Step 3: Review & sign */} + {arbStep === 3 && pendingArbTrade && ( +
+
+
πŸ“‹ Trade Order β€” Awaiting Your Signature
+ {[ + { label: 'Pair', value: selectedPair.symbol }, + { label: 'Action', value: `Buy on ${pendingArbTrade.buyEx} Β· Sell on ${pendingArbTrade.sellEx}` }, + { label: 'Size', value: `${orderAmount} units` }, + { label: 'Est. Profit', value: `+$${fmtPrice(pendingArbTrade.spread * parseFloat(orderAmount || '1'))}`, color: 'text-[#4ade80]' }, + { label: 'Network', value: network === 'mainnet' ? '🌐 Mainnet (real funds)' : 'πŸ”΅ Testnet (simulated)', color: network === 'mainnet' ? 'text-[#4ade80]' : 'text-blue-300' }, + { label: 'Agent Fee', value: '0.1 XLM via 0x402', color: 'text-[#FFB800]' }, + ].map((row) => ( +
+ {row.label} + {row.value} +
+ ))} +
+
+ ⚠ Your wallet signature authorises this trade. The agent will not proceed without it. +
+
+ + +
+
+ )} + + {/* Step 4: Signing in progress */} + {arbStep === 4 && sigModalOpen && ( +
+
✍
+
Awaiting wallet signature…
+
Please confirm the transaction in your wallet
+
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+
+ )} + + {/* Step 5: Result */} + {arbStep === 5 && arbResult && ( +
+
= 0 ? 'border-green-700 bg-green-900/10' : 'border-red-900 bg-red-900/10'}`}> +
{arbResult.profit >= 0 ? '🎯' : 'πŸ“‰'}
+
= 0 ? 'text-[#4ade80]' : 'text-red-400'}`}> + {arbResult.profit >= 0 ? '+' : ''}${fmtPrice(arbResult.profit)} +
+
+ {arbResult.profit >= 0 ? 'Arbitrage profit captured' : 'Spread closed before execution'} Β· {arbResult.pair.toUpperCase()} +
+
Dashboard PnL updated βœ“
+
+ +
+ )} +
+ + {/* How it works explainer */} +
+

How Arbitrage Agents Work

+
+ {[ + { step: '1', icon: 'πŸ”', title: 'Scan Prices', desc: 'Agent polls multiple CEX APIs (Binance, Coinbase, Kraken) every second for the same asset price.' }, + { step: '2', icon: 'πŸ“Š', title: 'Detect Spread', desc: 'When price difference exceeds a configurable threshold (e.g. 0.2%), the opportunity is flagged.' }, + { step: '3', icon: '✍', title: 'User Validates', desc: 'Agent presents trade details. You review and sign with your wallet β€” the agent never holds your keys.' }, + { step: '4', icon: '⚑', title: 'Execute & Profit', desc: 'Agent simultaneously buys on the cheaper exchange and sells on the pricier one, capturing the spread.' }, + ].map((item) => ( +
+
+ {item.icon} + {item.title} +
+

{item.desc}

+
+ ))} +
+
+
+ )} )} diff --git a/app/workflow/page.tsx b/app/workflow/page.tsx index a1ce0e0..f7e9250 100644 --- a/app/workflow/page.tsx +++ b/app/workflow/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useRef, useEffect, useCallback } from 'react'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -206,6 +206,544 @@ function ToolBtn({ ); } +// ─── Payment Executor Types ──────────────────────────────────────────────────── + +interface AgentInfo { + id: string; + name: string; + description?: string; + priceXlm: number; + ownerAddress: string; +} + +interface InvoiceData { + invoiceNumber: string; + agentId: string; + agentName: string; + task: string; + priceXlm: number; + txHash: string; + fromWallet: string; + timestamp: string; + explorerUrl: string; +} + +type ExecutorStep = + | 'idle' + | 'checking_wallet' + | 'building_tx' + | 'signing' + | 'submitting' + | 'confirming' + | 'running_agent' + | 'done' + | 'error'; + +const EXECUTOR_STEP_LABELS: Record = { + idle: 'Execute Task', + checking_wallet: 'Checking wallet…', + building_tx: 'Building transaction…', + signing: 'Waiting for Freighter…', + submitting: 'Submitting to Stellar…', + confirming: 'Confirming on ledger…', + running_agent: 'Running agent…', + done: 'Done', + error: 'Retry', +}; + +const STELLAR_MEMO_MAX_LENGTH = 28; +const STELLAR_POLL_INTERVAL_MS = 2_000; + +function generateInvoiceNumber(): string { + return `INV-${Date.now().toString(36).toUpperCase()}`; +} + +function extractStellarError(err: unknown): string { + if (!err) return 'Unknown error'; + if (typeof err === 'object' && err !== null) { + const e = err as Record; + try { + const resultCodes = ( + (e.response as Record)?.data as Record + )?.extras as Record; + if (resultCodes?.result_codes) { + const rc = resultCodes.result_codes as Record; + return `Transaction failed: ${rc.transaction || ''} ops: ${JSON.stringify(rc.operations || [])}`; + } + } catch { /* fall through */ } + } + const msg = String(err); + if (msg.includes('Resource Missing') || msg.includes('404')) + return 'Account not found on Stellar network. Make sure Freighter is funded on the correct network.'; + if (msg.includes('403') || msg.includes('Forbidden')) + return 'Access denied. Please unlock Freighter and try again.'; + return msg.startsWith('Error:') ? msg.slice(7).trim() : msg; +} + +async function waitForLedger( + horizonServer: import('stellar-sdk').Horizon.Server, + txHash: string, + timeoutMs = 30_000 +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + await horizonServer.transactions().transaction(txHash).call(); + return; + } catch { /* not yet */ } + await new Promise((r) => setTimeout(r, STELLAR_POLL_INTERVAL_MS)); + } +} + +// ─── Payment Executor Section ────────────────────────────────────────────────── + +function PaymentExecutorSection({ walletAddress }: { walletAddress: string }) { + const [agents, setAgents] = useState([]); + const [loadingAgents, setLoadingAgents] = useState(false); + const [agentsError, setAgentsError] = useState(null); + + const [selectedAgent, setSelectedAgent] = useState(null); + const [taskPrompt, setTaskPrompt] = useState(''); + + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [step, setStep] = useState('idle'); + const [stepError, setStepError] = useState(null); + const [invoice, setInvoice] = useState(null); + + useEffect(() => { + if (!walletAddress) return; + setLoadingAgents(true); + setAgentsError(null); + fetch(`/api/agents/list?owner=${walletAddress}`) + .then((r) => r.json()) + .then((data) => { + const list: AgentInfo[] = (Array.isArray(data) ? data : data?.agents ?? []).map( + (a: Record) => ({ + id: String(a.id ?? a.agent_id ?? ''), + name: String(a.name ?? a.agent_name ?? 'Unnamed Agent'), + description: a.description ? String(a.description) : undefined, + priceXlm: Number(a.price_xlm ?? a.priceXlm ?? 0.1), + ownerAddress: String(a.owner_address ?? a.ownerAddress ?? walletAddress), + }) + ); + setAgents(list); + }) + .catch((e) => setAgentsError(String(e))) + .finally(() => setLoadingAgents(false)); + }, [walletAddress]); + + const handleExecute = async () => { + if (!selectedAgent || !taskPrompt.trim()) return; + setShowConfirmModal(false); + setStep('checking_wallet'); + setStepError(null); + setInvoice(null); + + try { + const StellarSdk = await import('stellar-sdk'); + const freighter = await import('@stellar/freighter-api'); + + const connResult = await freighter.isConnected(); + if (!connResult.isConnected) + throw new Error('Freighter wallet is not installed. Visit https://www.freighter.app'); + + const accessResult = await freighter.requestAccess(); + if (accessResult && 'error' in accessResult && accessResult.error) + throw new Error('Freighter access denied. Please allow this site in Freighter.'); + + const { address: senderKey, error: addrErr } = await freighter.getAddress(); + if (addrErr || !senderKey) + throw new Error('Could not get wallet address. Ensure Freighter is unlocked.'); + + setStep('building_tx'); + + const isMainnet = process.env.NEXT_PUBLIC_STELLAR_NETWORK === 'mainnet'; + const horizonUrl = + process.env.NEXT_PUBLIC_HORIZON_URL ?? + (isMainnet ? 'https://horizon.stellar.org' : 'https://horizon-testnet.stellar.org'); + const networkPassphrase = isMainnet ? StellarSdk.Networks.PUBLIC : StellarSdk.Networks.TESTNET; + const horizonServer = new StellarSdk.Horizon.Server(horizonUrl); + + const senderAccount = await horizonServer.loadAccount(senderKey); + const memo = `agent:${selectedAgent.id}`.slice(0, STELLAR_MEMO_MAX_LENGTH); + + const tx = new StellarSdk.TransactionBuilder(senderAccount, { + fee: StellarSdk.BASE_FEE, + networkPassphrase, + }) + .addOperation( + StellarSdk.Operation.payment({ + destination: selectedAgent.ownerAddress, + asset: StellarSdk.Asset.native(), + amount: selectedAgent.priceXlm.toFixed(7), + }) + ) + .addMemo(StellarSdk.Memo.text(memo)) + .setTimeout(60) + .build(); + + setStep('signing'); + + const signedResult = await freighter.signTransaction(tx.toXDR(), { networkPassphrase }); + if (signedResult.error) throw new Error(String(signedResult.error)); + + const signedTx = StellarSdk.TransactionBuilder.fromXDR(signedResult.signedTxXdr, networkPassphrase); + + setStep('submitting'); + const submitResult = await horizonServer.submitTransaction(signedTx); + const txHash = submitResult.hash; + + setStep('confirming'); + await waitForLedger(horizonServer, txHash); + + setStep('running_agent'); + + const explorerNet = isMainnet ? 'public' : 'testnet'; + const explorerUrl = `https://stellar.expert/explorer/${explorerNet}/tx/${txHash}`; + + const runRes = await fetch(`/api/agents/${selectedAgent.id}/run`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-payment-tx-hash': txHash, + 'x-payment-from': senderKey, + }, + body: JSON.stringify({ prompt: taskPrompt, task: taskPrompt }), + }); + + const runData = await runRes.json().catch(() => ({})); + if (!runRes.ok) { + // Payment succeeded; agent run had an issue β€” still show invoice + console.warn('Agent run error:', runData); + } + + setInvoice({ + invoiceNumber: generateInvoiceNumber(), + agentId: selectedAgent.id, + agentName: selectedAgent.name, + task: taskPrompt, + priceXlm: selectedAgent.priceXlm, + txHash, + fromWallet: senderKey, + timestamp: new Date().toISOString(), + explorerUrl, + }); + + setStep('done'); + } catch (err) { + setStepError(extractStellarError(err)); + setStep('error'); + } + }; + + const busy = step !== 'idle' && step !== 'done' && step !== 'error'; + const isMainnet = process.env.NEXT_PUBLIC_STELLAR_NETWORK === 'mainnet'; + + return ( + + {/* Heading */} +
+
+ + + +
+
+

0x402 Payment Executor

+

Select an agent, enter a task, pay & execute via Stellar.

+
+
+ +
+ {/* ── Agent Selection ── */} +
+

Your Agents

+ + {loadingAgents && ( +
+ + Loading agents… +
+ )} + + {agentsError && ( +
+ {agentsError} +
+ )} + + {!loadingAgents && !agentsError && agents.length === 0 && ( +
+

No agents found for this wallet.

+

Deploy an agent from the dashboard first.

+
+ )} + +
+ {agents.map((agent, i) => ( + { setSelectedAgent(agent); setStep('idle'); setStepError(null); setInvoice(null); }} + className={`w-full text-left p-3 rounded-xl border transition-all ${ + selectedAgent?.id === agent.id + ? 'border-[rgba(0,255,229,0.4)] bg-[rgba(0,255,229,0.06)]' + : 'border-[rgba(255,255,255,0.06)] bg-[rgba(255,255,255,0.02)] hover:border-[rgba(0,255,229,0.2)] hover:bg-[rgba(0,255,229,0.03)]' + }`} + > +
+ + {agent.name} + + {agent.priceXlm} XLM +
+ {agent.description && ( +

{agent.description}

+ )} +

{agent.id}

+
+ ))} +
+
+ + {/* ── Task Input + Execute ── */} +
+

Task / Prompt

+ +