SAP DOCv0.20.0
Explorer

API Routes

Server-side API endpoints that bridge the SAP SDK to the explorer frontend. SDK v0.20.0 aligned.

API Routes

Explorer Version: 1.0.0
SDK Version: v0.20.0

The explorer exposes server-side API routes under src/app/api/ that bridge the SAP SDK to client components. All SDK interactions happen server-side; the frontend only receives serialized JSON.


Route Structure

src/app/api/
├── cluster/         GET    Network cluster info
├── supply/          GET    Token supply data
├── gateway/
│   ├── agents/      GET    List gateway agents
│   ├── execute/     POST   Execute agent command
│   ├── marketplace/ GET    Agent marketplace data
│   ├── pricing/     GET    Agent pricing info
│   └── session/     POST   Manage gateway sessions
├── defi/
│   ├── balance/     GET    Wallet token balances
│   ├── jupiter/     GET    Jupiter integration
│   ├── tokens/      GET    Token list
│   ├── quote/       GET    Swap quotes
│   └── swap/        POST   Execute swaps
├── nft/
│   └── [owner]/     GET    NFT holdings by owner
├── wallet/
│   └── [address]/   GET    Wallet details
├── agent/
│   └── chat/        POST   Agent chat interface
└── x402/
    ├── facilitators/ GET   x402 facilitators
    ├── paywall/      POST  Paywall check
    ├── prepare-tx/   POST  Prepare payment TX
    └── simulate/     POST  Simulate payment

Design Principles

Server-Only SDK

The SapClient singleton is instantiated in API routes, never exposed to the browser. This protects RPC credentials and private keys.

// src/lib/sap/client.ts
import { SapClient } from "@oobe-protocol-labs/synapse-sap-sdk";
import { AnchorProvider } from "@coral-xyz/anchor";

let _client: SapClient | null = null;

export function getSapClient(): SapClient {
  if (!_client) {
    const provider = AnchorProvider.env();
    _client = SapClient.from(provider);
  }
  return _client;
}

Error Handling

Every route wraps SDK calls in try/catch and returns proper JSON error responses with appropriate HTTP status codes:

// src/app/api/gateway/agents/route.ts
import { NextResponse } from "next/server";
import { getSapClient } from "@/lib/sap/discovery";
import { SapError, SapRpcError } from "@oobe-protocol-labs/synapse-sap-sdk";

export async function GET() {
  try {
    const client = getSapClient();
    const agents = await client.discovery.findAgentsByProtocol("A2A");

    return NextResponse.json({
      agents: agents.map((a) => ({
        pda: a.pda.toBase58(),
        name: a.identity?.name,
        active: a.identity?.isActive,
        reputation: a.computed?.reputationScore,
      })),
    });
  } catch (error: unknown) {
    if (error instanceof SapRpcError) {
      return NextResponse.json(
        { error: "RPC unavailable", code: "RPC_ERROR" },
        { status: 503 }
      );
    }
    
    if (error instanceof SapError) {
      return NextResponse.json(
        { error: error.message, code: error.constructor.name },
        { status: 400 }
      );
    }

    console.error("[API] Gateway agents failed:", error);
    return NextResponse.json(
      { error: "Internal server error", code: "INTERNAL" },
      { status: 500 }
    );
  }
}

Caching

Routes use Next.js revalidation with configurable TTLs to reduce RPC load:

// src/app/api/cluster/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  const cluster = process.env.NEXT_PUBLIC_SOLANA_CLUSTER || "mainnet-beta";
  const rpcUrl = process.env.RPC_URL || "";

  // Cache for 5 minutes
  return NextResponse.json(
    {
      cluster,
      rpcUrl: rpcUrl.replace(/\?api_key=.+/, "?api_key=***"),
      timestamp: Date.now(),
    },
    {
      headers: {
        "Cache-Control": "public, s-maxage=300, stale-while-revalidate=60",
      },
    }
  );
}

Type Safety

Request and response types are defined in TypeScript. API routes validate inputs before passing them to the SDK:

// src/app/api/defi/swap/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

const SwapRequestSchema = z.object({
  inputMint: z.string(),
  outputMint: z.string(),
  amount: z.number().positive(),
  slippageBps: z.number().min(0).max(1000).optional(),
});

export async function POST(request: NextRequest) {
  const body = await request.json();
  const validation = SwapRequestSchema.safeParse(body);

  if (!validation.success) {
    return NextResponse.json(
      { error: "Invalid request", details: validation.error.errors },
      { status: 400 }
    );
  }

  const { inputMint, outputMint, amount, slippageBps } = validation.data;
  // ... proceed with validated input
}

Example Route Patterns

Agent Discovery

// src/app/api/gateway/agents/route.ts
import { NextResponse } from "next/server";
import { getSapClient } from "@/lib/sap/discovery";

export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url);
    const protocol = searchParams.get("protocol") || "A2A";
    const limit = parseInt(searchParams.get("limit") || "50");

    const client = getSapClient();
    const agents = await client.discovery.findAgentsByProtocol(protocol);

    return NextResponse.json({
      agents: agents.slice(0, limit).map((a) => ({
        pda: a.pda.toBase58(),
        name: a.identity?.name,
        active: a.identity?.isActive,
        reputation: a.computed?.reputationScore,
        totalCalls: a.stats?.totalCallsServed.toString(),
      })),
      total: agents.length,
      protocol,
    });
  } catch (error) {
    console.error("[API] Agent discovery failed:", error);
    return NextResponse.json(
      { error: "Failed to fetch agents" },
      { status: 500 }
    );
  }
}

Wallet Details

// src/app/api/wallet/[address]/route.ts
import { NextResponse } from "next/server";
import { PublicKey } from "@solana/web3.js";
import { getSapClient } from "@/lib/sap/client";

export async function GET(
  request: Request,
  { params }: { params: { address: string } }
) {
  try {
    const address = new PublicKey(params.address);
    const client = getSapClient();

    // Check if wallet has a registered agent
    const agent = await client.agent.fetchNullable(address);

    // Get escrow balances
    const escrows = await client.escrow.listByDepositor(address);

    return NextResponse.json({
      address: address.toBase58(),
      hasAgent: agent !== null,
      agent: agent
        ? {
            name: agent.name,
            active: agent.isActive,
            reputation: agent.reputationScore,
          }
        : null,
      escrows: escrows.length,
      totalEscrowBalance: escrows.reduce(
        (sum, e) => sum.add(e.balance),
        new BN(0)
      ).toString(),
    });
  } catch (error) {
    if (error instanceof Error && error.message.includes("invalid")) {
      return NextResponse.json(
        { error: "Invalid wallet address" },
        { status: 400 }
      );
    }

    console.error("[API] Wallet details failed:", error);
    return NextResponse.json(
      { error: "Failed to fetch wallet details" },
      { status: 500 }
    );
  }
}

x402 Payment Simulation

// src/app/api/x402/simulate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getSapClient } from "@/lib/sap/client";
import { BN } from "@coral-xyz/anchor";

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { agentWallet, calls, pricePerCall } = body;

    const client = getSapClient();

    // Estimate cost with volume curve
    const estimate = await client.x402.estimateCost(
      new PublicKey(agentWallet),
      calls
    );

    // Simulate escrow creation
    const simulation = await client.x402.preparePayment(
      new PublicKey(agentWallet),
      {
        pricePerCall: new BN(pricePerCall),
        maxCalls: calls,
        deposit: estimate.totalCost,
        expiresAt: Math.floor(Date.now() / 1000) + 3600,
      },
      { simulate: true }
    );

    return NextResponse.json({
      estimatedCost: estimate.totalCost.toString(),
      effectivePricePerCall: estimate.effectivePricePerCall.toString(),
      hasVolumeCurve: estimate.hasVolumeCurve,
      simulation: {
        success: simulation.success,
        computeUnits: simulation.computeUnits,
        logs: simulation.logs,
      },
    });
  } catch (error) {
    console.error("[API] x402 simulation failed:", error);
    return NextResponse.json(
      { error: "Simulation failed", details: String(error) },
      { status: 400 }
    );
  }
}

Rate Limiting

Implement rate limiting to protect RPC endpoints:

// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const rateLimit = new Map<string, { count: number; resetAt: number }>();

export function middleware(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for") || "unknown";
  const now = Date.now();
  const windowMs = 60000; // 1 minute
  const maxRequests = 100;

  const record = rateLimit.get(ip) || { count: 0, resetAt: now + windowMs };

  if (now > record.resetAt) {
    record.count = 0;
    record.resetAt = now + windowMs;
  }

  record.count++;
  rateLimit.set(ip, record);

  if (record.count > maxRequests) {
    return NextResponse.json(
      { error: "Rate limit exceeded", retryAfter: record.resetAt - now },
      { status: 429 }
    );
  }

  return NextResponse.next();
}

export const config = {
  matcher: "/api/:path*",
};

RPC Connection Management

Use connection pooling and fallback strategies:

// src/lib/sap/connection.ts
import { Connection, clusterApiUrl } from "@solana/web3.js";
import { createDualConnection } from "@oobe-protocol-labs/synapse-sap-sdk";

const cluster = process.env.NEXT_PUBLIC_SOLANA_CLUSTER || "mainnet-beta";
const primaryRpc = process.env.RPC_URL || clusterApiUrl(cluster);
const fallbackRpc = process.env.FALLBACK_RPC_URL;

export const connection = createDualConnection({
  primaryUrl: primaryRpc,
  fallbackUrl: fallbackRpc,
  cluster,
});

Monitoring and Logging

Implement structured logging for debugging:

// src/lib/logger.ts
export function logApiCall(
  route: string,
  method: string,
  duration: number,
  status: number,
  error?: string
) {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    route,
    method,
    duration,
    status,
    error,
  }));
}

// Usage in API route
const start = Date.now();
try {
  // ... handler logic
  logApiCall("/api/gateway/agents", "GET", Date.now() - start, 200);
} catch (error) {
  logApiCall("/api/gateway/agents", "GET", Date.now() - start, 500, String(error));
  throw error;
}

Next Steps


Last Updated: June 2026
Explorer Version: 1.0.0
SDK Version: 0.20.0