Open-Core Integration Guide · v2.5 · Central Node Architecture

Network Tab
Integration Guide

A complete, self-contained implementation guide for adding a Network tab to any self-hosted DiviDen open-core instance — enabling federation registration with dividen.ai, automated heartbeating, Bubble Store marketplace participation, and agent sync.

Audience: AI agents or human developers implementing federation on any single self-hosted DiviDen instance. Each instance (e.g., Pickleclaw, Inyo, Klaw Voice) is fully independent — it has its own database, its own platformToken, its own agents. This guide applies to one instance at a time. If you manage multiple instances, repeat this process separately for each one.

1. Architecture Overview

DiviDen federation uses a central node architecture. The managed instance at dividen.ai serves as the central coordination node — the single source of truth for the network registry, the Bubble Store marketplace, and cross-instance relay routing. Self-hosted open-core instances connect to the central node to join the network. Each connected instance maintains full autonomy over its own data, agents, and operations while gaining access to shared network services.

                ┌─────────────────────────────────────┐
                │     dividen.ai (Central Node)        │
                │  ┌──────────┐  ┌─────────────────┐   │
                │  │ Network   │  │ Bubble Store    │   │
                │  │ Registry  │  │ Marketplace     │   │
                │  └──────────┘  └─────────────────┘   │
                │  ┌──────────────────────────────┐    │
                │  │ Federation APIs /api/v2/fed/* │    │
                │  │ Relay · Discovery · Sync      │    │
                │  └──────────────────────────────┘    │
                └───────┬──────────┬──────────┬────────┘
                        │          │          │
               ┌────────┴┐   ┌────┴─────┐  ┌─┴────────┐
               │ Instance │   │ Instance │  │ Instance │
               │ Pickle-  │   │ Inyo     │  │ Klaw     │
               │ claw     │   │          │  │ Voice    │
               └──────────┘   └──────────┘  └──────────┘

  Each instance is FULLY AUTONOMOUS:
    • Own database, own .env, own platformToken
    • No shared state between instances
    • Each registers independently with the
      central node, even if run by the same dev

  Per-instance lifecycle:
    1. Connects → registers with central node
    2. Heartbeats every 5–15 min
    3. Enables marketplace → lists its agents
    4. Syncs agents → appear in Bubble Store
Critical:All API Calls Target dividen.ai
Every federation API call from your instance goes to https://dividen.ai/api/v2/federation/*. Do NOT call your own instance's federation endpoints — that triggers the self-registration guard and returns HTTP 422.

What the Network Tab Provides

Registration Wizard
One-click registration with dividen.ai. Handles the /register API call and stores the returned platformToken.
Heartbeat Monitor
Shows heartbeat status, configures interval, displays last-seen timestamp from the central node.
Marketplace Toggle
Enable/disable Bubble Store participation. When enabled, your agents can be synced to the marketplace.
Agent Sync Panel
Lists local agents, shows sync status, and pushes selected agents to the Bubble Store with one click.
Connection Status
Real-time display of federation status: pending, active, features enabled, token validity.
Configuration
Instance identity (name, URL), federation mode, inbound/outbound toggles, API key management.

2. Prerequisites

DiviDen open-core instance running and accessible via a public URL (HTTPS required)
Prisma schema includes the InstanceRegistry and FederationConfig models (standard in v2.1+)
An admin user account on the instance
At least one agent defined locally that you want to sync to the Bubble Store(recommended)
Node.js 18+ with Next.js 14 (standard DiviDen stack)
The .well-known/agent-card.json endpoint serving a valid DAWP agent card(recommended)

Prisma Schema Verification

Your schema should include these models. If missing, add them and run npx prisma db push.

prisma/schema.prismaprisma
model FederationConfig {
  id               String   @id @default(cuid())
  instanceName     String   @default("DiviDen Instance")
  instanceUrl      String?  // Your public URL, e.g. https://cc.fractionalventure.partners
  federationMode   String   @default("closed")  // "closed" | "allowlist" | "open"
  allowInbound     Boolean  @default(false)
  allowOutbound    Boolean  @default(false)
  requireApproval  Boolean  @default(true)
  instanceApiKey   String?  // Your federation API key (for inbound auth)
  createdAt        DateTime @default(now())
  updatedAt        DateTime @updatedAt
}

model InstanceRegistry {
  id                 String    @id @default(cuid())
  name               String
  baseUrl            String    @unique
  apiKey             String
  isActive           Boolean   @default(false)
  isTrusted          Boolean   @default(false)
  lastSeenAt         DateTime?
  platformLinked     Boolean   @default(false)
  platformToken      String?   // Token returned by dividen.ai on registration
  marketplaceEnabled Boolean   @default(false)
  discoveryEnabled   Boolean   @default(false)
  updatesEnabled     Boolean   @default(false)
  version            String?
  userCount          Int?
  agentCount         Int?
  lastSyncAt         DateTime?
  metadata           String?   // JSON blob for capabilities, etc.
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt
}

3. Environment Configuration

Add these variables to your .env file. The DIVIDEN_PLATFORM_TOKEN will be populated automatically after registration, but the other values must be set beforehand.

.envenv
# ── Federation Configuration ─────────────────────────────────
# Your instance's public name (displayed on dividen.ai)
DIVIDEN_INSTANCE_NAME="Fractional Venture Partners"

# Your instance's public URL (must be HTTPS, must be reachable from dividen.ai)
DIVIDEN_INSTANCE_URL="https://cc.fractionalventure.partners"

# The central node URL (do not change unless using a different central node)
DIVIDEN_HUB_URL="https://dividen.ai"

# Platform token — populated after successful registration
# This is the Bearer token for all central node API calls
DIVIDEN_PLATFORM_TOKEN=""

# Instance ID — populated after successful registration  
DIVIDEN_INSTANCE_ID=""

# Heartbeat interval in milliseconds (default: 5 minutes)
DIVIDEN_HEARTBEAT_INTERVAL="300000"

# Auto-sync agents to marketplace on startup (true/false)
DIVIDEN_AUTO_SYNC_AGENTS="false"
Warning:Token Security
The DIVIDEN_PLATFORM_TOKEN is a long-lived Bearer token that authenticates your instance with dividen.ai. Treat it like a database password — never commit it to version control, never expose it in client-side code, never log it.
Critical:One Token Per Instance
Each DiviDen instance gets its own unique DIVIDEN_PLATFORM_TOKEN and DIVIDEN_INSTANCE_ID during registration. Do NOT share tokens between instances. If you operate Pickleclaw, Inyo, and Klaw Voice, each one has its own .env with its own token. There is no cross-instance token reuse.

4. Backend API Routes

Your instance needs three local API routes that the Network tab UI calls. These routes act as a proxy/orchestrator — they call the dividen.ai federation APIs on your behalf so that theDIVIDEN_PLATFORM_TOKEN never reaches the browser.

1

GET /api/network/status

Returns the current federation status of this instance. The Network tab polls this on mount.

src/app/api/network/status/route.tsTypeScript
export const dynamic = 'force-dynamic';

import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';

export async function GET() {
  const session = await getServerSession(authOptions);
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const config = await prisma.federationConfig.findFirst();
  const platformToken = process.env.DIVIDEN_PLATFORM_TOKEN;
  const instanceId = process.env.DIVIDEN_INSTANCE_ID;

  // Check if we have a valid registration
  const isRegistered = !!(platformToken && instanceId);

  // If registered, try to verify by checking our instance record on the central node
  let hubStatus: 'connected' | 'pending' | 'disconnected' = 'disconnected';
  let hubFeatures = { marketplace: false, discovery: false, relay: false, updates: false };
  let lastSeen: string | null = null;

  if (isRegistered) {
    try {
      // Use heartbeat as a status check
      const res = await fetch(
        `${process.env.DIVIDEN_HUB_URL || 'https://dividen.ai'}/api/v2/federation/heartbeat`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${platformToken}`,
          },
          body: JSON.stringify({
            version: process.env.npm_package_version || '2.1.0',
          }),
        }
      );
      if (res.ok) {
        const data = await res.json();
        hubStatus = data.instance?.isActive ? 'connected' : 'pending';
        hubFeatures = {
          marketplace: data.instance?.marketplaceEnabled || false,
          discovery: data.instance?.discoveryEnabled || false,
          relay: true,
          updates: data.instance?.updatesEnabled || false,
        };
        lastSeen = data.instance?.lastSeenAt || null;
      } else if (res.status === 403) {
        hubStatus = 'pending'; // Token valid but instance not approved
      }
    } catch {
      // Central node unreachable — still registered but disconnected
      hubStatus = isRegistered ? 'pending' : 'disconnected';
    }
  }

  return NextResponse.json({
    isRegistered,
    hubStatus,
    instanceId: instanceId || null,
    instanceName: config?.instanceName || process.env.DIVIDEN_INSTANCE_NAME || null,
    instanceUrl: config?.instanceUrl || process.env.DIVIDEN_INSTANCE_URL || null,
    hubUrl: process.env.DIVIDEN_HUB_URL || 'https://dividen.ai',
    features: hubFeatures,
    lastSeen,
    federationMode: config?.federationMode || 'closed',
    allowInbound: config?.allowInbound || false,
    allowOutbound: config?.allowOutbound || false,
  });
}
2

POST /api/network/register

Initiates registration with dividen.ai. Called once by the Network tab wizard.

src/app/api/network/register/route.tsTypeScript
export const dynamic = 'force-dynamic';

import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';

export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions);
  if (!session?.user || (session.user as any).role !== 'admin') {
    return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
  }

  const body = await req.json();
  const {
    instanceName,
    instanceUrl,
    enableMarketplace = true,
    enableDiscovery = true,
    enableUpdates = true,
  } = body;

  if (!instanceName || !instanceUrl) {
    return NextResponse.json(
      { error: 'instanceName and instanceUrl are required' },
      { status: 400 }
    );
  }

  const hubUrl = process.env.DIVIDEN_HUB_URL || 'https://dividen.ai';

  // Ensure we have a federation config
  let config = await prisma.federationConfig.findFirst();
  if (!config) {
    config = await prisma.federationConfig.create({
      data: {
        instanceName,
        instanceUrl: instanceUrl.replace(//$/, ''),
        federationMode: 'allowlist',
        allowInbound: true,
        allowOutbound: true,
        requireApproval: true,
        instanceApiKey: crypto.randomBytes(32).toString('hex'),
      },
    });
  } else {
    config = await prisma.federationConfig.update({
      where: { id: config.id },
      data: {
        instanceName,
        instanceUrl: instanceUrl.replace(//$/, ''),
        allowInbound: true,
        allowOutbound: true,
      },
    });
  }

  // ── Call dividen.ai /api/v2/federation/register ──
  const registerRes = await fetch(`${hubUrl}/api/v2/federation/register`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: instanceName,
      baseUrl: instanceUrl.replace(//$/, ''),
      apiKey: config.instanceApiKey || 'self-register',
      version: process.env.npm_package_version || '2.1.0',
      capabilities: {
        relay: true,
        marketplace: enableMarketplace,
        discovery: enableDiscovery,
        updates: enableUpdates,
      },
    }),
  });

  if (!registerRes.ok) {
    const err = await registerRes.json().catch(() => ({ error: 'Registration failed' }));
    return NextResponse.json(
      { error: err.error || `HTTP ${registerRes.status}` },
      { status: registerRes.status }
    );
  }

  const result = await registerRes.json();

  // ── Store the platformToken in .env ──
  // In production, you may want to store this in a secrets manager instead
  const envPath = path.join(process.cwd(), '.env');
  try {
    let envContent = '';
    try { envContent = fs.readFileSync(envPath, 'utf-8'); } catch { /* file may not exist */ }

    const setEnv = (key: string, value: string) => {
      const regex = new RegExp(`^${key}=.*$`, 'm');
      if (regex.test(envContent)) {
        envContent = envContent.replace(regex, `${key}="${value}"`);
      } else {
        envContent += `\n${key}="${value}"`;
      }
    };

    setEnv('DIVIDEN_PLATFORM_TOKEN', result.platformToken);
    setEnv('DIVIDEN_INSTANCE_ID', result.instanceId);
    fs.writeFileSync(envPath, envContent);

    // Also set in process.env for immediate use
    process.env.DIVIDEN_PLATFORM_TOKEN = result.platformToken;
    process.env.DIVIDEN_INSTANCE_ID = result.instanceId;
  } catch (envErr) {
    console.error('[Network Register] Failed to write .env:', envErr);
    // Non-fatal — return the token so the user can set it manually
  }

  // ── If marketplace enabled, activate it ──
  if (enableMarketplace && result.platformToken) {
    try {
      await fetch(`${hubUrl}/api/v2/federation/marketplace-link`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${result.platformToken}`,
        },
        body: JSON.stringify({ action: 'enable' }),
      });
    } catch { /* non-blocking */ }
  }

  return NextResponse.json({
    success: true,
    instanceId: result.instanceId,
    platformToken: result.platformToken,
    status: result.status, // 'pending_approval' or 'active'
    endpoints: result.endpoints,
    features: result.features,
    message: result.message,
  });
}
3

POST /api/network/sync-agents

Pushes local agents to dividen.ai's Bubble Store. Called by the Agent Sync panel.

src/app/api/network/sync-agents/route.tsTypeScript
export const dynamic = 'force-dynamic';

import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';

export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions);
  if (!session?.user || (session.user as any).role !== 'admin') {
    return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
  }

  const platformToken = process.env.DIVIDEN_PLATFORM_TOKEN;
  if (!platformToken) {
    return NextResponse.json(
      { error: 'Not registered. Complete registration first.' },
      { status: 400 }
    );
  }

  const hubUrl = process.env.DIVIDEN_HUB_URL || 'https://dividen.ai';
  const body = await req.json();
  const { agentIds } = body; // Optional: specific agent IDs to sync

  // Fetch local agents (adapt this query to your schema)
  const where: any = { isPublished: true };
  if (agentIds?.length) where.id = { in: agentIds };

  const localAgents = await prisma.marketplaceAgent.findMany({
    where,
    include: { developer: { select: { name: true, email: true } } },
  });

  if (localAgents.length === 0) {
    return NextResponse.json({ error: 'No published agents found to sync' }, { status: 400 });
  }

  // Transform to the federation agent sync format
  const agents = localAgents.map((a: any) => ({
    id: a.id,
    name: a.name,
    description: a.description,
    longDescription: a.longDescription || null,
    category: a.category || 'general',
    tags: a.tags || '',
    endpointUrl: a.endpointUrl || `${process.env.DIVIDEN_INSTANCE_URL}/api/a2a`,
    developerName: a.developer?.name || a.developerName || 'Unknown',
    developerUrl: a.developerUrl || process.env.DIVIDEN_INSTANCE_URL || '',
    inputFormat: a.inputFormat || 'text',
    outputFormat: a.outputFormat || 'text',
    pricingModel: a.pricingModel || 'free',
    pricePerTask: a.pricePerTask || null,
    version: a.version || '1.0.0',
    supportsA2A: a.supportsA2A || false,
    supportsMCP: a.supportsMCP || false,
    agentCardUrl: a.agentCardUrl || null,
    // Include capability metadata if available
    capabilities: {
      taskTypes: a.taskTypes || null,
      contextInstructions: a.contextInstructions || null,
    },
  }));

  // Push to dividen.ai
  const syncRes = await fetch(`${hubUrl}/api/v2/federation/agents`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${platformToken}`,
    },
    body: JSON.stringify({ agents }),
  });

  if (!syncRes.ok) {
    const err = await syncRes.json().catch(() => ({ error: 'Sync failed' }));
    return NextResponse.json(
      { error: err.error || `HTTP ${syncRes.status}`, code: err.code },
      { status: syncRes.status }
    );
  }

  const syncResult = await syncRes.json();
  return NextResponse.json(syncResult);
}

// GET — check sync status
export async function GET() {
  const session = await getServerSession(authOptions);
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const platformToken = process.env.DIVIDEN_PLATFORM_TOKEN;
  if (!platformToken) {
    return NextResponse.json({ synced: false, agents: [] });
  }

  const hubUrl = process.env.DIVIDEN_HUB_URL || 'https://dividen.ai';

  try {
    const res = await fetch(`${hubUrl}/api/v2/federation/agents`, {
      headers: { 'Authorization': `Bearer ${platformToken}` },
    });
    if (res.ok) {
      const data = await res.json();
      return NextResponse.json({ synced: true, ...data });
    }
  } catch { /* ignore */ }

  return NextResponse.json({ synced: false, agents: [] });
}

5. Network Tab UI Component

This is the core React component. It provides the full Network tab experience — status display, registration wizard, marketplace toggle, and agent sync panel. Drop this into your components directory.

Note:Styling Assumptions
This component uses DiviDen's CSS variable system (--text-primary, --bg-primary, --border-color, --brand-primary, etc.) and Tailwind utility classes. If your fork uses a different theme system, adapt the class names accordingly.
src/components/network/NetworkTab.tsxTypeScript (React)
'use client';

import { useState, useEffect, useCallback } from 'react';

// ── Types ──────────────────────────────────────────────────────

interface NetworkStatus {
  isRegistered: boolean;
  hubStatus: 'connected' | 'pending' | 'disconnected';
  instanceId: string | null;
  instanceName: string | null;
  instanceUrl: string | null;
  hubUrl: string;
  features: {
    marketplace: boolean;
    discovery: boolean;
    relay: boolean;
    updates: boolean;
  };
  lastSeen: string | null;
  federationMode: string;
  allowInbound: boolean;
  allowOutbound: boolean;
}

interface RegisterResult {
  success: boolean;
  instanceId: string;
  platformToken: string;
  status: string;
  endpoints: Record<string, string>;
  features: Record<string, boolean>;
  message: string;
}

interface SyncedAgent {
  remoteId: string;
  name: string;
  status: string;
  marketplaceUrl?: string;
}

type WizardStep = 'idle' | 'form' | 'registering' | 'success' | 'error';

// ── Component ──────────────────────────────────────────────────

export function NetworkTab() {
  // Status
  const [status, setStatus] = useState<NetworkStatus | null>(null);
  const [loading, setLoading] = useState(true);

  // Registration wizard
  const [wizardStep, setWizardStep] = useState<WizardStep>('idle');
  const [wizardError, setWizardError] = useState('');
  const [registerResult, setRegisterResult] = useState<RegisterResult | null>(null);
  const [regForm, setRegForm] = useState({
    instanceName: '',
    instanceUrl: '',
    enableMarketplace: true,
    enableDiscovery: true,
    enableUpdates: true,
  });

  // Agent sync
  const [syncedAgents, setSyncedAgents] = useState<SyncedAgent[]>([]);
  const [syncing, setSyncing] = useState(false);
  const [syncResult, setSyncResult] = useState<any>(null);

  // ── Fetch status on mount ──
  const fetchStatus = useCallback(async () => {
    try {
      const res = await fetch('/api/network/status');
      if (res.ok) {
        const data = await res.json();
        setStatus(data);
        // Pre-fill registration form from config
        if (data.instanceName) setRegForm(f => ({ ...f, instanceName: data.instanceName }));
        if (data.instanceUrl) setRegForm(f => ({ ...f, instanceUrl: data.instanceUrl }));
      }
    } catch (e) { console.error('Failed to fetch network status:', e); }
    finally { setLoading(false); }
  }, []);

  const fetchSyncStatus = useCallback(async () => {
    try {
      const res = await fetch('/api/network/sync-agents');
      if (res.ok) {
        const data = await res.json();
        if (data.agents) setSyncedAgents(data.agents);
      }
    } catch { /* ignore */ }
  }, []);

  useEffect(() => {
    fetchStatus();
    fetchSyncStatus();
  }, [fetchStatus, fetchSyncStatus]);

  // ── Registration handler ──
  const handleRegister = async () => {
    if (!regForm.instanceName || !regForm.instanceUrl) {
      setWizardError('Instance name and URL are required.');
      setWizardStep('error');
      return;
    }

    setWizardStep('registering');
    setWizardError('');

    try {
      const res = await fetch('/api/network/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(regForm),
      });

      if (!res.ok) {
        const err = await res.json().catch(() => ({ error: 'Registration failed' }));
        throw new Error(err.error || `HTTP ${res.status}`);
      }

      const result = await res.json();
      setRegisterResult(result);
      setWizardStep('success');

      // Refresh status
      setTimeout(fetchStatus, 2000);
    } catch (err: any) {
      setWizardError(err.message || 'Registration failed');
      setWizardStep('error');
    }
  };

  // ── Agent sync handler ──
  const handleSyncAgents = async () => {
    setSyncing(true);
    setSyncResult(null);
    try {
      const res = await fetch('/api/network/sync-agents', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({}), // Sync all published agents
      });
      const data = await res.json();
      setSyncResult(data);
      if (res.ok) fetchSyncStatus();
    } catch (err: any) {
      setSyncResult({ error: err.message });
    } finally { setSyncing(false); }
  };

  // ── Loading state ──
  if (loading) {
    return (
      <div className="flex items-center justify-center py-16">
        <div className="text-sm text-[var(--text-muted)] animate-pulse">
          Loading network status...
        </div>
      </div>
    );
  }

  const isConnected = status?.hubStatus === 'connected';
  const isPending = status?.hubStatus === 'pending';
  const isRegistered = status?.isRegistered;

  return (
    <div className="space-y-6 max-w-3xl">
      {/* ── Status Banner ── */}
      <div className={`p-5 rounded-xl border ${
        isConnected
          ? 'bg-emerald-500/5 border-emerald-500/20'
          : isPending
          ? 'bg-amber-500/5 border-amber-500/20'
          : 'bg-[var(--bg-secondary)] border-[var(--border-color)]'
      }`}>
        <div className="flex items-center gap-3 mb-2">
          <div className={`w-3 h-3 rounded-full ${
            isConnected ? 'bg-emerald-400 animate-pulse'
            : isPending ? 'bg-amber-400 animate-pulse'
            : 'bg-white/20'
          }`} />
          <span className="text-base font-semibold text-[var(--text-primary)]">
            {isConnected ? 'Connected to DiviDen Network'
             : isPending ? '⏳ Registration Pending Approval'
             : 'Not Connected'}
          </span>
        </div>
        <p className="text-xs text-[var(--text-muted)] leading-relaxed">
          {isConnected
            ? `Connected to ${status?.hubUrl}. Your agents can participate in the Bubble Store marketplace.`
            : isPending
            ? 'Your registration is pending admin approval on dividen.ai. Heartbeat is active — you will be notified when approved.'
            : 'Register your instance with dividen.ai to join the network and access the Bubble Store, discovery feed, and relay network.'}
        </p>
        {isConnected && status?.features && (
          <div className="flex gap-2 mt-3">
            {status.features.marketplace && (
              <span className="text-[10px] px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400">
                Bubble Store
              </span>
            )}
            {status.features.discovery && (
              <span className="text-[10px] px-2 py-0.5 rounded bg-blue-500/10 text-blue-400">
                Discovery
              </span>
            )}
            {status.features.relay && (
              <span className="text-[10px] px-2 py-0.5 rounded bg-purple-500/10 text-purple-400">
                Relay
              </span>
            )}
          </div>
        )}
      </div>

      {/* ── Registration Wizard (shown when not registered) ── */}
      {!isRegistered && (
        <div className="p-5 rounded-xl bg-[var(--bg-secondary)] border border-[var(--border-color)]">
          <h3 className="text-sm font-semibold text-[var(--text-primary)] mb-1">
            Join the DiviDen Network
          </h3>
          <p className="text-xs text-[var(--text-muted)] mb-4">
            Register this instance with dividen.ai to access the agent marketplace,
            network discovery, and cross-instance relay.
          </p>

          {wizardStep === 'idle' && (
            <button
              onClick={() => setWizardStep('form')}
              className="w-full py-2.5 text-sm font-medium rounded-lg
                         bg-[var(--brand-primary)] text-white
                         hover:bg-[var(--brand-primary)]/80 transition-colors"
            >
              Start Registration →
            </button>
          )}

          {wizardStep === 'form' && (
            <div className="space-y-3">
              <div>
                <label className="text-[11px] text-[var(--text-muted)] block mb-1">
                  Instance Name
                </label>
                <input
                  value={regForm.instanceName}
                  onChange={e => setRegForm(f => ({ ...f, instanceName: e.target.value }))}
                  placeholder="e.g. Fractional Venture Partners"
                  className="w-full px-3 py-2 text-sm bg-[var(--bg-primary)]
                             border border-[var(--border-color)] rounded-lg
                             text-[var(--text-primary)]
                             placeholder:text-[var(--text-muted)]
                             focus:outline-none focus:border-[var(--brand-primary)]/50"
                />
              </div>
              <div>
                <label className="text-[11px] text-[var(--text-muted)] block mb-1">
                  Public URL (must be reachable from the internet)
                </label>
                <input
                  value={regForm.instanceUrl}
                  onChange={e => setRegForm(f => ({ ...f, instanceUrl: e.target.value }))}
                  placeholder="https://cc.yourcompany.com"
                  className="w-full px-3 py-2 text-sm bg-[var(--bg-primary)]
                             border border-[var(--border-color)] rounded-lg
                             text-[var(--text-primary)] font-mono
                             placeholder:text-[var(--text-muted)]
                             focus:outline-none focus:border-[var(--brand-primary)]/50"
                />
              </div>

              {/* Feature toggles */}
              <div className="space-y-2 pt-2">
                <span className="text-[11px] text-[var(--text-muted)]">Enable features:</span>
                {[
                  { key: 'enableMarketplace' as const, label: 'Bubble Store', desc: 'List your agents on the marketplace' },
                  { key: 'enableDiscovery' as const, label: 'Discovery', desc: 'Browse the network discovery feed' },
                  { key: 'enableUpdates' as const, label: 'Updates', desc: 'Receive platform updates' },
                ].map(f => (
                  <label key={f.key} className="flex items-start gap-3 cursor-pointer p-2 rounded-lg hover:bg-[var(--bg-tertiary)]">
                    <input
                      type="checkbox"
                      checked={regForm[f.key]}
                      onChange={e => setRegForm(prev => ({ ...prev, [f.key]: e.target.checked }))}
                      className="mt-0.5 rounded"
                    />
                    <div>
                      <span className="text-xs text-[var(--text-primary)]">{f.label}</span>
                      <p className="text-[10px] text-[var(--text-muted)]">{f.desc}</p>
                    </div>
                  </label>
                ))}
              </div>

              <div className="flex gap-2 pt-2">
                <button
                  onClick={() => setWizardStep('idle')}
                  className="flex-1 py-2 text-xs rounded-lg bg-[var(--bg-surface)]
                             border border-[var(--border-color)] text-[var(--text-secondary)]
                             hover:text-[var(--text-primary)] transition-colors"
                >
                  Cancel
                </button>
                <button
                  onClick={handleRegister}
                  disabled={!regForm.instanceName || !regForm.instanceUrl}
                  className="flex-1 py-2 text-xs font-medium rounded-lg
                             bg-[var(--brand-primary)] text-white
                             hover:bg-[var(--brand-primary)]/80 transition-colors
                             disabled:opacity-50"
                >
                  Register with dividen.ai
                </button>
              </div>
            </div>
          )}

          {wizardStep === 'registering' && (
            <div className="text-center py-8">
              <div className="text-3xl mb-3 animate-pulse"></div>
              <p className="text-sm text-[var(--text-primary)]">Registering with dividen.ai...</p>
              <p className="text-[11px] text-[var(--text-muted)] mt-1">
                Exchanging keys and configuring federation
              </p>
            </div>
          )}

          {wizardStep === 'success' && registerResult && (
            <div className="space-y-4">
              <div className="text-center">
                <div className="text-3xl mb-2"></div>
                <h4 className="text-sm font-semibold text-emerald-400">
                  Registration {registerResult.status === 'pending_approval' ? 'Submitted' : 'Complete'}
                </h4>
                <p className="text-xs text-[var(--text-muted)] mt-1">
                  {registerResult.message}
                </p>
              </div>

              {registerResult.status === 'pending_approval' && (
                <div className="p-3 rounded-lg bg-amber-500/5 border border-amber-500/20">
                  <p className="text-xs text-amber-400">
                    ⏳ Your registration is pending admin approval on dividen.ai.
                    The heartbeat service will start automatically. You&apos;ll
                    transition to &quot;connected&quot; once approved.
                  </p>
                </div>
              )}

              <div className="p-3 rounded-lg bg-[var(--bg-primary)] border border-[var(--border-color)]">
                <div className="text-[10px] text-[var(--text-muted)] font-semibold mb-2">
                  PLATFORM TOKEN (saved to .env automatically)
                </div>
                <div className="px-3 py-2 bg-black/30 rounded text-[10px] font-mono
                                text-[var(--text-secondary)] break-all select-all">
                  {registerResult.platformToken}
                </div>
                <p className="text-[10px] text-amber-400/80 mt-2">
                  This token was saved to your .env file as DIVIDEN_PLATFORM_TOKEN.
                  If your deployment system doesn&apos;t read .env at runtime, add it
                  to your secrets manager.
                </p>
              </div>

              <div className="p-3 rounded-lg bg-[var(--bg-primary)] border border-[var(--border-color)]">
                <div className="text-[10px] text-[var(--text-muted)] font-semibold mb-2">
                  AVAILABLE ENDPOINTS
                </div>
                <div className="space-y-1">
                  {Object.entries(registerResult.endpoints || {}).map(([key, url]) => (
                    <div key={key} className="flex items-center gap-2 text-[11px]">
                      <span className="text-emerald-400">→</span>
                      <span className="text-[var(--text-muted)] w-28">{key}:</span>
                      <span className="font-mono text-[var(--text-secondary)] text-[10px]">
                        {url}
                      </span>
                    </div>
                  ))}
                </div>
              </div>

              <button
                onClick={() => { setWizardStep('idle'); fetchStatus(); }}
                className="w-full py-2 text-xs rounded-lg bg-[var(--bg-surface)]
                           border border-[var(--border-color)] text-[var(--text-secondary)]
                           hover:text-[var(--text-primary)] transition-colors"
              >
                Done
              </button>
            </div>
          )}

          {wizardStep === 'error' && (
            <div className="space-y-3">
              <div className="text-center">
                <div className="text-3xl mb-2"></div>
                <h4 className="text-sm font-semibold text-red-400">Registration Failed</h4>
                <p className="text-xs text-[var(--text-muted)] mt-1">{wizardError}</p>
              </div>
              <div className="flex gap-2">
                <button
                  onClick={() => setWizardStep('idle')}
                  className="flex-1 py-2 text-xs rounded-lg bg-[var(--bg-surface)]
                             border border-[var(--border-color)] text-[var(--text-secondary)]"
                >
                  Cancel
                </button>
                <button
                  onClick={() => setWizardStep('form')}
                  className="flex-1 py-2 text-xs font-medium rounded-lg
                             bg-[var(--brand-primary)] text-white"
                >
                  Try Again
                </button>
              </div>
            </div>
          )}
        </div>
      )}

      {/* ── Agent Sync Panel (shown when registered) ── */}
      {isRegistered && (
        <div className="p-5 rounded-xl bg-[var(--bg-secondary)] border border-[var(--border-color)]">
          <div className="flex items-center justify-between mb-4">
            <div>
              <h3 className="text-sm font-semibold text-[var(--text-primary)]">
                Agent Sync
              </h3>
              <p className="text-xs text-[var(--text-muted)]">
                Push your published agents to the Bubble Store on dividen.ai
              </p>
            </div>
            <button
              onClick={handleSyncAgents}
              disabled={syncing || !isConnected}
              className="px-4 py-2 text-xs font-medium rounded-lg
                         bg-[var(--brand-primary)] text-white
                         hover:bg-[var(--brand-primary)]/80 transition-colors
                         disabled:opacity-50"
            >
              {syncing ? 'Syncing...' : '↑ Sync Agents'}
            </button>
          </div>

          {!isConnected && (
            <p className="text-xs text-amber-400 mb-3">
              Agent sync requires an active connection. Wait for admin approval
              on dividen.ai before syncing.
            </p>
          )}

          {syncResult && (
            <div className={`p-3 rounded-lg mb-3 ${
              syncResult.error
                ? 'bg-red-500/5 border border-red-500/20'
                : 'bg-emerald-500/5 border border-emerald-500/20'
            }`}>
              {syncResult.error ? (
                <p className="text-xs text-red-400">{syncResult.error}</p>
              ) : (
                <div>
                  <p className="text-xs text-emerald-400 mb-1">Sync complete</p>
                  <p className="text-[10px] text-[var(--text-muted)]">
                    {syncResult.created || 0} created · {syncResult.updated || 0} updated · {syncResult.skipped || 0} skipped
                  </p>
                  <p className="text-[10px] text-amber-400/80 mt-1">
                    All synced agents enter &quot;pending_review&quot; on dividen.ai.
                    A platform admin must approve each agent before it appears
                    in the Bubble Store.
                  </p>
                </div>
              )}
            </div>
          )}

          {syncedAgents.length > 0 && (
            <div className="space-y-1.5">
              <div className="text-[10px] text-[var(--text-muted)] font-semibold">SYNCED AGENTS</div>
              {syncedAgents.map((a, i) => (
                <div key={i} className="flex items-center justify-between px-3 py-2 rounded-lg bg-[var(--bg-secondary)]">
                  <span className="text-xs text-[var(--text-primary)]">{a.name}</span>
                  <span className={`text-[10px] px-2 py-0.5 rounded ${
                    a.status === 'approved' ? 'bg-emerald-500/10 text-emerald-400'
                    : a.status === 'pending_review' ? 'bg-amber-500/10 text-amber-400'
                    : 'bg-[var(--bg-surface)] text-[var(--text-muted)]'
                  }`}>
                    {a.status}
                  </span>
                </div>
              ))}
            </div>
          )}
        </div>
      )}

      {/* ── Connection Details (shown when registered) ── */}
      {isRegistered && status && (
        <div className="p-5 rounded-xl bg-[var(--bg-secondary)] border border-[var(--border-color)]">
          <h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
            Connection Details
          </h3>
          <div className="space-y-2 text-xs">
            <div className="flex justify-between">
              <span className="text-[var(--text-muted)]">Instance ID</span>
              <span className="font-mono text-[var(--text-secondary)]">
                {status.instanceId || '—'}
              </span>
            </div>
            <div className="flex justify-between">
              <span className="text-[var(--text-muted)]">Central Node</span>
              <span className="font-mono text-[var(--text-secondary)]">
                {status.hubUrl}
              </span>
            </div>
            <div className="flex justify-between">
              <span className="text-[var(--text-muted)]">Status</span>
              <span className={`font-medium ${
                isConnected ? 'text-emerald-400'
                : isPending ? 'text-amber-400'
                : 'text-red-400'
              }`}>
                {status.hubStatus}
              </span>
            </div>
            <div className="flex justify-between">
              <span className="text-[var(--text-muted)]">Last Heartbeat</span>
              <span className="text-[var(--text-secondary)]">
                {status.lastSeen ? new Date(status.lastSeen).toLocaleString() : 'Never'}
              </span>
            </div>
            <div className="flex justify-between">
              <span className="text-[var(--text-muted)]">Federation Mode</span>
              <span className="text-[var(--text-secondary)]">
                {status.federationMode}
              </span>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

6. Heartbeat Service

The heartbeat tells dividen.ai that your instance is alive. Without it, your instance will show as "stale" on the central node and may be de-prioritized in discovery results. There are two approaches:

Option A: In-App Interval (Recommended for Next.js)

Set up a Next.js API route that gets called by a client-side interval or by a cron trigger. This is the simplest approach — add it to your existing app without external infrastructure.

src/app/api/network/heartbeat/route.tsTypeScript
export const dynamic = 'force-dynamic';

import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';

/**
 * POST /api/network/heartbeat
 * Called internally (cron, setInterval, or from the Network tab)
 * to send a heartbeat to dividen.ai.
 */
export async function POST() {
  const platformToken = process.env.DIVIDEN_PLATFORM_TOKEN;
  if (!platformToken) {
    return NextResponse.json({ error: 'Not registered' }, { status: 400 });
  }

  const hubUrl = process.env.DIVIDEN_HUB_URL || 'https://dividen.ai';

  // Gather instance stats
  const [userCount, agentCount] = await Promise.all([
    prisma.user.count().catch(() => 0),
    prisma.marketplaceAgent.count({ where: { isPublished: true } }).catch(() => 0),
  ]);

  try {
    const res = await fetch(`${hubUrl}/api/v2/federation/heartbeat`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${platformToken}`,
      },
      body: JSON.stringify({
        version: process.env.npm_package_version || '2.1.0',
        userCount,
        agentCount,
        status: { uptime: process.uptime?.() || 0 },
      }),
    });

    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      console.error('[Heartbeat] Failed:', err);
      return NextResponse.json({ success: false, error: err.error }, { status: res.status });
    }

    const data = await res.json();
    return NextResponse.json({ success: true, ...data });
  } catch (err: any) {
    console.error('[Heartbeat] Network error:', err.message);
    return NextResponse.json({ success: false, error: err.message }, { status: 500 });
  }
}

Then add a client-side heartbeat trigger in your app layout or Network tab:

src/lib/heartbeat.tsTypeScript
// Call this once on app startup (e.g., in a useEffect in your root layout)
export function startHeartbeat() {
  const interval = parseInt(process.env.NEXT_PUBLIC_HEARTBEAT_INTERVAL || '300000', 10);

  const beat = () => {
    fetch('/api/network/heartbeat', { method: 'POST' })
      .catch(err => console.warn('[Heartbeat] Failed:', err.message));
  };

  // Initial heartbeat after 10s delay (let the app settle)
  setTimeout(beat, 10000);
  // Then every 5 minutes (or configured interval)
  setInterval(beat, interval);
}

Option B: External Cron Job

If you prefer not to run the heartbeat inside the app, use an external cron service.

crontab -e
# Every 5 minutes — heartbeat to dividen.ai
*/5 * * * * curl -s -X POST https://dividen.ai/api/v2/federation/heartbeat \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $DIVIDEN_PLATFORM_TOKEN" \
  -d '{"version":"2.1.0"}' > /dev/null 2>&1

7. Agent Sync to Bubble Store

Agent sync pushes your local agents to dividen.ai so they appear in the Bubble Store marketplace. Key rules:

Your instance must be TRUSTED (isTrusted: true) on dividen.ai before agent sync works. This is set by the dividen.ai admin after registration.
Marketplace must be ENABLED for your instance. Call /api/v2/federation/marketplace-link with action: "enable" (the registration wizard does this automatically).
ALL synced agents enter "pending_review" status on dividen.ai. A platform admin must approve each agent before it appears publicly in the Bubble Store.
Re-syncing an agent updates its listing. The agent is matched by your instance ID + remote agent ID.
Maximum 50 agents per sync call. If you have more, batch them.

Agent Sync Payload Format

Each agent in the agents array should include:

AgentSyncPayloadTypeScript
interface AgentSyncPayload {
  // ── Required ──
  id: string;              // Your local agent ID
  name: string;            // Agent display name
  description: string;     // Short description (1-2 sentences)

  // ── Recommended ──
  endpointUrl?: string;    // A2A/MCP endpoint URL
  developerName?: string;  // Attribution name
  developerUrl?: string;   // Developer website
  category?: string;       // "research" | "coding" | "writing" | "analysis" |
                           // "operations" | "creative" | "general"
  tags?: string;           // Comma-separated
  longDescription?: string;// Rich markdown description
  version?: string;        // Semver (e.g., "1.0.0")

  // ── Pricing ──
  pricingModel?: string;   // "free" | "per_task" | "tiered" | "dynamic"
  pricePerTask?: number;   // $ per execution (for per_task model)
  currency?: string;       // ISO 4217 (default: "USD")

  // ── Capabilities ──
  inputFormat?: string;    // "text" | "json" | "a2a"
  outputFormat?: string;   // "text" | "json" | "a2a"
  supportsA2A?: boolean;
  supportsMCP?: boolean;
  agentCardUrl?: string;   // .well-known/agent-card.json URL
  capabilities?: {
    taskTypes?: string;    // JSON array or comma-separated
    contextInstructions?: string;
  };
}

8. Full Registration Flow

Here's the complete sequence from initial setup to agents live in the Bubble Store:

1

Configure Instance Identity

Set DIVIDEN_INSTANCE_NAME and DIVIDEN_INSTANCE_URL in your .env. These identify your instance to dividen.ai.

The URL must be publicly reachable over HTTPS. dividen.ai will verify it can reach your instance.

2

Register with dividen.ai

Click "Start Registration" in the Network tab. This calls POST /api/v2/federation/register on dividen.ai.

Returns a platformToken and instanceId. The token is automatically saved to your .env file.

3

Wait for Admin Approval

New registrations start as pending_approval. A dividen.ai admin must set isActive: true and isTrusted: true.

You can verify your status by checking the Network tab — it will update from "Pending" to "Connected" once approved.

4

Heartbeat Starts Automatically

The heartbeat service sends periodic pings to dividen.ai, keeping your instance visible in the network.

Default interval is every 5 minutes. dividen.ai marks instances as stale if no heartbeat is received for 30 minutes.

5

Enable Marketplace (Automatic)

The registration wizard enables marketplace participation by default (calls /api/v2/federation/marketplace-link).

You can toggle this on/off later via the Network tab or by calling the marketplace-link endpoint directly.

6

Sync Agents

Click "Sync Agents" in the Network tab to push your published agents to the Bubble Store.

Each agent enters pending_review. Contact the dividen.ai admin (Jon) to approve your agents.

7

Agents Go Live

Once approved, your agents appear in the Bubble Store at dividen.ai/marketplace.

Users across the network can discover, install, and execute tasks against your agents. Cross-instance relay handles the communication.

9. Dashboard Integration

There are two places the Network tab should appear in the open-core UI:

A. Dashboard Center Panel — Network Group

In the main dashboard, add a "Network" button to the top tab bar. On dividen.ai, this group includes: Discover, Connections, Teams, Tasks, Federation Intel. For the open-core version, the Network group should include at minimum: Network Status (the NetworkTab component above) andDiscover (pulling from the central node's discovery feed).

src/components/dashboard/CenterPanel.tsx (add to tab bar)TypeScript
// Add to your primary tabs or as a grouped dropdown:
const networkTabs = [
  { id: 'network-status', label: 'Network', icon: '' },
  { id: 'discover', label: 'Discover', icon: '' },
  { id: 'marketplace', label: 'Bubble Store', icon: '' },
];

// In the tab content area:
{activeTab === 'network-status' && <NetworkTab />}

B. Settings → Network Tab

The Settings page should include a "Network" tab with the full federation configuration (federation mode, inbound/outbound toggles, API key management) plus the NetworkTab component for registration and status.

src/app/settings/page.tsx (add Network tab)TypeScript
// In your settings tab list:
{ id: 'network', label: 'Network', icon: '' }

// In the tab content area:
{activeTab === 'network' && (
  <div className="space-y-6">
    <div className="panel">
      <div className="panel-header">
        <h2 className="font-semibold">Network</h2>
        <p className="text-xs text-[var(--text-muted)] mt-0.5">
          Connect to the DiviDen network and manage federation settings.
        </p>
      </div>
      <div className="panel-body">
        <NetworkTab />
      </div>
    </div>
  </div>
)}

10. Troubleshooting

Registration returns HTTP 422 "Self-registration is not allowed"

You are calling your own instance's register endpoint instead of dividen.ai's. Make sure DIVIDEN_HUB_URL is set to https://dividen.ai and your /api/network/register route calls the central node URL, not localhost.

Registration returns HTTP 403 "API key mismatch"

Your instance was previously registered with a different apiKey. Ask the dividen.ai admin to delete the old instance record, or use the same apiKey that was used during initial registration.

Heartbeat returns 403

Your platformToken is invalid or expired. Re-register to get a new token. The /api/network/register route handles re-registration automatically.

Agent sync returns "marketplace_not_enabled"

Call /api/v2/federation/marketplace-link with action: "enable" first. The registration wizard does this automatically, but if you registered manually, you need to enable it separately.

Agent sync returns "instance_not_trusted"

Agent sync requires isTrusted: true on your instance record at dividen.ai. Ask the platform admin to trust your instance.

Agents synced but not visible in Bubble Store

All synced agents start in pending_review status. A dividen.ai admin must approve each agent. Contact the platform admin.

Status shows "pending" indefinitely

Your registration is waiting for admin approval on dividen.ai. The admin must set isActive: true on your instance. Keep heartbeating — the admin will see your instance is alive.

CORS errors when calling dividen.ai APIs

Federation APIs should be called from your backend (API routes), not from the browser. The Network tab calls your local /api/network/* routes, which then proxy to dividen.ai server-side.

11. API Reference (dividen.ai Endpoints)

All endpoints below are on https://dividen.ai. Auth header format: Authorization: Bearer <platformToken>

POST/api/v2/federation/register

Register your instance. Returns platformToken and instanceId.

Auth: None (public)
Body: { name, baseUrl, apiKey, version?, capabilities? }
Response: { instanceId, platformToken, status, endpoints, features }
POST/api/v2/federation/heartbeat

Send heartbeat. Updates lastSeenAt on the central node.

Auth: Bearer <platformToken>
Body: { version?, userCount?, agentCount?, status? }
Response: { success, instance: { isActive, lastSeenAt, ... } }
POST/api/v2/federation/marketplace-link

Enable/disable marketplace participation.

Auth: Bearer <platformToken>
Body: { action: "enable" | "disable" | "status" }
Response: { marketplaceEnabled, message }
POST/api/v2/federation/agents

Push agents to the Bubble Store. Max 50 per call.

Auth: Bearer <platformToken> (requires isTrusted)
Body: { agents: AgentSyncPayload[] }
Response: { results: [{ remoteId, status, marketplaceId? }], created, updated, skipped }
GET/api/v2/federation/agents

List agents synced from your instance.

Auth: Bearer <platformToken>
Body: None
Response: { agents: [{ id, name, status, marketplaceUrl? }] }
POST/api/v2/federation/execute

Execute a task on a remote agent via the central node (cross-instance relay).

Auth: Bearer <platformToken>
Body: { agentId, task, input, ... }
Response: { taskId, status, result? }

12. Verification Checklist

Use this checklist to verify your federation integration is working correctly:

Setup
FederationConfig and InstanceRegistry models exist in Prisma schema
.env has DIVIDEN_INSTANCE_NAME, DIVIDEN_INSTANCE_URL, and DIVIDEN_HUB_URL
Instance is publicly accessible over HTTPS
API Routes
GET /api/network/status returns valid JSON with isRegistered, hubStatus fields
POST /api/network/register successfully calls dividen.ai and returns platformToken
POST /api/network/sync-agents pushes agents and returns results
POST /api/network/heartbeat sends heartbeat and returns success
UI
Network tab appears in the dashboard and/or settings
Status banner shows correct state (disconnected → pending → connected)
Registration wizard completes without errors
Agent sync panel lists local agents and shows sync results
Runtime
Heartbeat fires every 5 minutes after registration
DIVIDEN_PLATFORM_TOKEN is stored in .env and accessible in process.env
Agent sync creates pending_review entries on dividen.ai
Re-registration preserves existing approval status
Done:All Done?
Once all items are checked, your instance is fully federated into the DiviDen network. Your agents will be discoverable on the Bubble Store, your instance will appear in the network directory, and cross-instance relay communication will be active. Contact Jon Bradford (jon@dividen.ai) or the platform admin channel to get your instance approved and your agents reviewed.

Related: Federation Setup (curl guide) · Federation Protocol · Relay Specification · Agent Integration Kit

Download a plain-text copy of this page