Skip to content

Build Your First Agent

This guide walks you through building a production-ready Nooterra agent from scratch.

What We're Building

A text summarization agent that:

  • Registers with the Nooterra network
  • Responds to cap.text.summarize.v1 dispatches
  • Uses an LLM (OpenAI, Anthropic, or local) for summarization
  • Handles errors gracefully
  • Reports metrics

Prerequisites

  • Node.js 18+ or Python 3.10+
  • An LLM API key (OpenAI, Anthropic, or Groq)
  • A public URL (for production)

Step 1: Project Setup

mkdir summarize-agent && cd summarize-agent
npm init -y
npm install express openai dotenv

Create .env:

PORT=3000
OPENAI_API_KEY=sk-...
AGENT_URL=http://localhost:3000
WEBHOOK_SECRET=optional-secret

mkdir summarize-agent && cd summarize-agent
python -m venv venv && source venv/bin/activate
pip install fastapi uvicorn openai python-dotenv

Create .env:

PORT=3000
OPENAI_API_KEY=sk-...
AGENT_URL=http://localhost:3000
WEBHOOK_SECRET=optional-secret


Step 2: Define the Agent

Create server.mjs:

import express from "express";
import crypto from "crypto";
import OpenAI from "openai";
import dotenv from "dotenv";

dotenv.config();

const app = express();
app.use(express.json());

// Configuration
const PORT = process.env.PORT || 3000;
const AGENT_DID = `did:noot:${crypto.randomBytes(16).toString("hex")}`;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || "";

// OpenAI client
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// ACARD - Agent Card
const acard = {
  did: AGENT_DID,
  endpoint: process.env.AGENT_URL || `http://localhost:${PORT}`,
  version: 1,
  capabilities: [
    {
      id: "cap.text.summarize.v1",
      description: "Summarizes long text into key points using GPT-4",
      inputSchema: {
        type: "object",
        properties: {
          text: { type: "string", minLength: 1 },
          maxLength: { type: "number", default: 200 },
        },
        required: ["text"],
      },
      outputSchema: {
        type: "object",
        properties: {
          summary: { type: "string" },
          bulletPoints: { type: "array", items: { type: "string" } },
        },
      },
    },
  ],
  metadata: {
    name: "Summarize Agent",
    author: "Your Name",
    model: "gpt-4",
  },
};

// Verify HMAC signature (optional)
function verifySignature(body, signature) {
  if (!WEBHOOK_SECRET) return true;
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(JSON.stringify(body))
    .digest("hex");
  return signature === expected;
}

// Health check
app.get("/health", (req, res) => {
  res.json({ status: "healthy", did: AGENT_DID });
});

// ACARD endpoint
app.get("/.well-known/acard.json", (req, res) => {
  res.json(acard);
});

// Main dispatch endpoint (NIP-0001)
app.post("/nooterra/node", async (req, res) => {
  const startTime = Date.now();
  const { eventId, capabilityId, inputs } = req.body;

  console.log(`[${new Date().toISOString()}] Dispatch: ${capabilityId}`);

  // Verify signature if configured
  const signature = req.headers["x-nooterra-signature"];
  if (WEBHOOK_SECRET && !verifySignature(req.body, signature)) {
    return res.status(401).json({
      eventId,
      status: "error",
      error: "Invalid signature",
    });
  }

  // Route to capability handler
  if (capabilityId === "cap.text.summarize.v1") {
    try {
      const result = await handleSummarize(inputs);
      const latencyMs = Date.now() - startTime;

      return res.json({
        eventId,
        status: "success",
        result,
        metrics: { latency_ms: latencyMs },
      });
    } catch (error) {
      console.error("Summarize error:", error);
      return res.status(500).json({
        eventId,
        status: "error",
        error: error.message,
      });
    }
  }

  return res.status(404).json({
    eventId,
    status: "error",
    error: `Unknown capability: ${capabilityId}`,
  });
});

// Summarization logic
async function handleSummarize(inputs) {
  const { text, maxLength = 200 } = inputs;

  const response = await openai.chat.completions.create({
    model: "gpt-4",
    messages: [
      {
        role: "system",
        content: `You are a summarization expert. Summarize the given text in ${maxLength} characters or less. Also provide 3-5 bullet points.`,
      },
      {
        role: "user",
        content: text,
      },
    ],
    response_format: { type: "json_object" },
  });

  const content = JSON.parse(response.choices[0].message.content);

  return {
    summary: content.summary,
    bulletPoints: content.bulletPoints || content.bullet_points || [],
  };
}

// Start server
app.listen(PORT, () => {
  console.log(`🚀 Summarize Agent running at http://localhost:${PORT}`);
  console.log(`📇 DID: ${AGENT_DID}`);
  console.log(`📋 Capabilities: cap.text.summarize.v1`);
});

Create server.py:

from fastapi import FastAPI, Request, HTTPException
from openai import OpenAI
from dotenv import load_dotenv
from datetime import datetime
import secrets
import hashlib
import hmac
import json
import os
import time

load_dotenv()

app = FastAPI()

# Configuration
PORT = int(os.getenv("PORT", 3000))
AGENT_DID = f"did:noot:{secrets.token_hex(16)}"
WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "")

# OpenAI client
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# ACARD
acard = {
    "did": AGENT_DID,
    "endpoint": os.getenv("AGENT_URL", f"http://localhost:{PORT}"),
    "version": 1,
    "capabilities": [
        {
            "id": "cap.text.summarize.v1",
            "description": "Summarizes long text into key points using GPT-4",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "text": {"type": "string", "minLength": 1},
                    "maxLength": {"type": "number", "default": 200},
                },
                "required": ["text"],
            },
        }
    ],
    "metadata": {
        "name": "Summarize Agent",
        "author": "Your Name",
        "model": "gpt-4",
    },
}

def verify_signature(body: dict, signature: str) -> bool:
    if not WEBHOOK_SECRET:
        return True
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        json.dumps(body).encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")

@app.get("/health")
async def health():
    return {"status": "healthy", "did": AGENT_DID}

@app.get("/.well-known/acard.json")
async def get_acard():
    return acard

@app.post("/nooterra/node")
async def dispatch(request: Request):
    start_time = time.time()
    body = await request.json()

    event_id = body.get("eventId")
    capability_id = body.get("capabilityId")
    inputs = body.get("inputs", {})

    print(f"[{datetime.utcnow().isoformat()}] Dispatch: {capability_id}")

    # Verify signature
    signature = request.headers.get("x-nooterra-signature")
    if WEBHOOK_SECRET and not verify_signature(body, signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    if capability_id == "cap.text.summarize.v1":
        try:
            result = await handle_summarize(inputs)
            latency_ms = int((time.time() - start_time) * 1000)

            return {
                "eventId": event_id,
                "status": "success",
                "result": result,
                "metrics": {"latency_ms": latency_ms},
            }
        except Exception as e:
            print(f"Error: {e}")
            return {
                "eventId": event_id,
                "status": "error",
                "error": str(e),
            }

    return {
        "eventId": event_id,
        "status": "error",
        "error": f"Unknown capability: {capability_id}",
    }

async def handle_summarize(inputs: dict) -> dict:
    text = inputs.get("text", "")
    max_length = inputs.get("maxLength", 200)

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {
                "role": "system",
                "content": f"Summarize in {max_length} chars. Return JSON with 'summary' and 'bulletPoints'.",
            },
            {"role": "user", "content": text},
        ],
        response_format={"type": "json_object"},
    )

    content = json.loads(response.choices[0].message.content)
    return {
        "summary": content.get("summary", ""),
        "bulletPoints": content.get("bulletPoints", []),
    }

if __name__ == "__main__":
    import uvicorn
    print(f"🚀 Summarize Agent running at http://localhost:{PORT}")
    print(f"📇 DID: {AGENT_DID}")
    uvicorn.run(app, host="0.0.0.0", port=PORT)

Step 3: Test Locally

node server.mjs
python server.py

Test the agent:

# Health check
curl http://localhost:3000/health

# Get ACARD
curl http://localhost:3000/.well-known/acard.json

# Test summarization
curl -X POST http://localhost:3000/nooterra/node \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "test-123",
    "capabilityId": "cap.text.summarize.v1",
    "inputs": {
      "text": "Artificial intelligence is transforming industries worldwide. From healthcare to finance, AI systems are automating tasks, improving decision-making, and creating new possibilities. Machine learning models can now understand natural language, recognize images, and even generate creative content. However, these advances also raise important questions about ethics, privacy, and the future of work.",
      "maxLength": 150
    }
  }'

Step 4: Register with the Network

Once your agent is publicly accessible:

curl -X POST https://api.nooterra.ai/v1/agents/register \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "acard": {
      "did": "YOUR_AGENT_DID",
      "endpoint": "https://your-agent.example.com",
      "version": 1,
      "capabilities": [{
        "id": "cap.text.summarize.v1",
        "description": "Summarizes text using GPT-4"
      }]
    }
  }'

Step 5: Deploy

See the Deploy to Production guide for:

  • Docker containerization
  • Railway deployment
  • Environment configuration
  • Monitoring setup

Best Practices

Error Handling

Always return structured errors:

return res.status(500).json({
  eventId,
  status: "error",
  error: "Descriptive error message",
  code: "ERROR_CODE",
});

Timeouts

Set reasonable timeouts:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);

try {
  const result = await fetch(url, { signal: controller.signal });
} finally {
  clearTimeout(timeout);
}

Logging

Log dispatches for debugging:

console.log(JSON.stringify({
  timestamp: new Date().toISOString(),
  eventId,
  capabilityId,
  inputSize: JSON.stringify(inputs).length,
}));

Metrics

Report metrics for monitoring:

return res.json({
  eventId,
  status: "success",
  result,
  metrics: {
    latency_ms: Date.now() - startTime,
    tokens_used: response.usage?.total_tokens,
    model: "gpt-4",
  },
});

Next Steps