Skip to content

Agent Client

The AgentClient class provides a simple way to interact with Agent APIs built with the AgentServer. It handles JWT authentication automatically and provides a clean interface for making API calls.

Overview

The AgentClient simplifies communication with Agent APIs by:

  • Automatic Authentication: Creates and manages JWT tokens using SR25519 signatures
  • Type-Safe Responses: Provides structured success/error responses
  • HTTP Method Support: Supports GET, POST, PUT, PATCH, and DELETE requests
  • Error Handling: Consistent error handling with detailed error information

Installation

Terminal window
npm install @torus-network/torus-ts-sdk

Basic Usage

import { AgentClient, Keypair } from "@torus-network/torus-ts-sdk";
// Create a keypair from your mnemonic
const keypair = new Keypair(
"your twelve word mnemonic phrase goes here exactly like this"
);
// Create the client
const client = new AgentClient({
keypair,
baseUrl: "http://localhost:3000",
});
// Make a call to the agent
const response = await client.call({
endpoint: "hello",
data: { name: "Alice" },
});
if (response.success) {
console.log("Response:", response.data);
} else {
console.error("Error:", response.error);
}

Configuration

AgentClientOptions

interface AgentClientOptions {
/** The keypair for creating JWT tokens */
keypair: Keypair;
/** The base URL of the agent server */
baseUrl: string;
}

Creating a Keypair

The Keypair class handles SR25519 key generation and JWT token creation:

import { Keypair } from "@torus-network/torus-ts-sdk";
// Create from mnemonic
const keypair = new Keypair(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
);
// Get key information
const keyInfo = await keypair.getKeyInfo();
console.log("Address:", keyInfo.address);
console.log("Public Key:", keyInfo.publicKey);

Making API Calls

Call Options

interface CallOptions {
/** The endpoint path (without leading slash) */
endpoint: string;
/** The HTTP method to use */
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; // Default: "POST"
/** The request body data */
data?: unknown;
}

Response Structure

interface CallResponse<T = unknown> {
/** Whether the request was successful */
success: boolean;
/** The response data (present if success is true) */
data?: T;
/** Error information (present if success is false) */
error?: {
message: string;
code?: string;
status: number;
};
}

HTTP Methods

// POST with data
const response = await client.call({
endpoint: "store-memory",
data: {
content: "Remember this important information",
tags: ["important", "meeting"]
}
});
if (response.success) {
console.log("Memory stored with ID:", response.data.id);
}

Error Handling

The AgentClient provides consistent error handling for various failure scenarios:

Network Errors

const response = await client.call({
endpoint: "test-endpoint",
data: { test: "data" }
});
if (!response.success) {
switch (response.error?.status) {
case 0:
console.error("Network error:", response.error.message);
break;
case 401:
console.error("Authentication failed:", response.error.message);
break;
case 403:
console.error("Access denied:", response.error.message);
break;
case 400:
console.error("Bad request:", response.error.message);
break;
default:
console.error("Unexpected error:", response.error.message);
}
}

Structured Error Responses

const response = await client.call({
endpoint: "risky-operation",
data: { action: "dangerous" },
});
if (!response.success && response.error?.code) {
switch (response.error.code) {
case "FORBIDDEN_ACTION":
console.error("This action is not allowed");
break;
case "NAMESPACE_ACCESS_DENIED":
console.error("Insufficient permissions");
break;
default:
console.error("Unknown error:", response.error.code);
}
}

Type Safety

Use TypeScript generics to ensure type safety for your API responses:

interface MemoryResponse {
id: string;
content: string;
timestamp: number;
tags: string[];
}
interface ErrorResponse {
error: string;
code: string;
}
// Type-safe API call
const response = await client.call<MemoryResponse>({
endpoint: "get-memory",
method: "GET",
data: { id: "memory-123" },
});
if (response.success) {
// response.data is now typed as MemoryResponse
console.log("Memory ID:", response.data.id);
console.log("Content:", response.data.content);
console.log("Tags:", response.data.tags);
}

Complete Examples

Memory Agent Client

import { AgentClient, Keypair } from "@torus-network/torus-ts-sdk";
class MemoryAgentClient {
private client: AgentClient;
constructor(mnemonic: string, baseUrl: string) {
const keypair = new Keypair(mnemonic);
this.client = new AgentClient({ keypair, baseUrl });
}
async storeMemory(
content: string,
tags?: string[],
metadata?: Record<string, any>
) {
const response = await this.client.call({
endpoint: "store",
data: { content, tags, metadata },
});
if (response.success) {
return {
success: true,
id: response.data.id,
timestamp: response.data.timestamp,
};
} else {
return {
success: false,
error: response.error?.message || "Failed to store memory",
};
}
}
async retrieveMemory(id: string) {
const response = await this.client.call({
endpoint: "retrieve",
method: "GET",
data: { id },
});
if (response.success) {
return {
success: true,
memory: response.data,
};
} else {
return {
success: false,
error: response.error?.message || "Failed to retrieve memory",
};
}
}
async searchMemories(query: string, tags?: string[], limit = 10) {
const response = await this.client.call({
endpoint: "search",
data: { query, tags, limit },
});
if (response.success) {
return {
success: true,
results: response.data.results,
total: response.data.total,
};
} else {
return {
success: false,
error: response.error?.message || "Search failed",
};
}
}
}
// Usage
const memoryClient = new MemoryAgentClient(
"your mnemonic phrase here",
"http://localhost:3000"
);
// Store a memory
const storeResult = await memoryClient.storeMemory(
"Important meeting notes from today",
["meeting", "important"],
{ date: "2024-01-15", attendees: ["Alice", "Bob"] }
);
if (storeResult.success) {
console.log("Memory stored:", storeResult.id);
// Retrieve the memory
const retrieveResult = await memoryClient.retrieveMemory(storeResult.id);
if (retrieveResult.success) {
console.log("Retrieved memory:", retrieveResult.memory);
}
}
// Search for memories
const searchResult = await memoryClient.searchMemories("meeting", [
"important",
]);
if (searchResult.success) {
console.log(`Found ${searchResult.total} memories`);
searchResult.results.forEach((result) => {
console.log(`- ${result.content} (score: ${result.score})`);
});
}

Environment-based Configuration

import { AgentClient, Keypair } from "@torus-network/torus-ts-sdk";
// Environment configuration
const config = {
mnemonic: process.env.AGENT_MNEMONIC || "",
baseUrl: process.env.AGENT_BASE_URL || "http://localhost:3000",
};
if (!config.mnemonic) {
throw new Error("AGENT_MNEMONIC environment variable is required");
}
const keypair = new Keypair(config.mnemonic);
const client = new AgentClient({
keypair,
baseUrl: config.baseUrl,
});
// Helper function for making calls with retry logic
async function callWithRetry<T>(
options: CallOptions,
maxRetries = 3
): Promise<CallResponse<T>> {
let lastError: any;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await client.call<T>(options);
if (response.success) {
return response;
}
// Don't retry on client errors (4xx)
if (
response.error?.status &&
response.error.status >= 400 &&
response.error.status < 500
) {
return response;
}
lastError = response.error;
// Wait before retry
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
}
}
return {
success: false,
error: {
message: lastError?.message || "Max retries exceeded",
status: lastError?.status || 0,
},
};
}
// Usage with retry
const response = await callWithRetry({
endpoint: "important-operation",
data: { critical: "data" },
});

Authentication Details

The AgentClient automatically handles JWT authentication:

  1. JWT Creation: Creates a JWT token using your keypair’s SR25519 signature
  2. Token Management: Tokens are created fresh for each request
  3. Protocol Metadata: Includes protocol version information for compatibility
  4. Automatic Headers: Adds the Authorization: Bearer <token> header to all requests

JWT Token Structure

{
"header": {
"alg": "SR25519",
"typ": "JWT"
},
"payload": {
"sub": "5FgfC2DY4yreEWEughz46RZYQ8oBhHVqD9fVq6gV89E6z4Ea", // Your address
"publicKey": "0x...", // Your public key
"keyType": "sr25519", // Key type
"addressInfo": {
"addressType": "ss58", // Address encoding type
"metadata": {
"prefix": 42 // SS58 prefix for Substrate generic
}
},
"iat": 1640995200, // Issued at timestamp
"exp": 1640998800, // Expires at timestamp
"nonce": "unique-uuid", // Prevents replay attacks
"_protocol_metadata": {
"version": "1.0.0" // Protocol version
}
}
}

Other Languages

If you’re building an Agent Client in a language other than TypeScript, you’ll need to implement the following protocol specifications:

JWT Token Generation

To authenticate with Agent Servers, you must generate JWT tokens with SR25519 signatures.

Required Components

  1. SR25519 Keypair: Generate from mnemonic seed phrase
  2. JWT Structure: Header + Payload + Signature
  3. Protocol Metadata: Version compatibility information

JWT Creation Process

import json
import base64
import time
import uuid
from sr25519 import sign, keypair_from_mnemonic # Use appropriate SR25519 library
class AgentClient:
def **init**(self, mnemonic: str, base_url: str):
self.keypair = keypair_from_mnemonic(mnemonic)
self.base_url = base_url.rstrip('/')
self.public_key = self.keypair.public_key.hex()
self.address = self.keypair.ss58_address
def create_jwt(self) -> str:
now = int(time.time())
# JWT Header
header = {
"alg": "SR25519",
"typ": "JWT"
}
# JWT Payload
payload = {
"sub": self.address,
"publicKey": f"0x{self.public_key}",
"keyType": "sr25519",
"addressInfo": {
"addressType": "ss58",
"metadata": {
"prefix": 42
}
},
"iat": now,
"exp": now + 3600, # 1 hour expiration
"nonce": str(uuid.uuid4()),
"_protocol_metadata": {
"version": "1.0.0"
}
}
# Encode header and payload
header_encoded = base64.urlsafe_b64encode(
json.dumps(header).encode()
).decode().rstrip('=')
payload_encoded = base64.urlsafe_b64encode(
json.dumps(payload).encode()
).decode().rstrip('=')
# Create signing input
signing_input = f"{header_encoded}.{payload_encoded}"
# Sign with SR25519
signature = sign(signing_input.encode(), self.keypair.secret_key)
signature_encoded = base64.urlsafe_b64encode(signature).decode().rstrip('=')
return f"{header_encoded}.{payload_encoded}.{signature_encoded}"
def call(self, endpoint: str, method: str = "POST", data: dict = None) -> dict:
import requests
jwt_token = self.create_jwt()
url = f"{self.base_url}/{endpoint.lstrip('/')}"
headers = {
"Authorization": f"Bearer {jwt_token}",
"Content-Type": "application/json"
}
try:
if method.upper() == "GET":
response = requests.get(url, headers=headers, params=data)
else:
response = requests.request(
method.upper(), url, headers=headers, json=data
)
if response.status_code == 200:
return {"success": True, "data": response.json()}
else:
error_data = response.json() if response.text else {}
return {
"success": False,
"error": {
"message": error_data.get("message", f"HTTP {response.status_code}"),
"code": error_data.get("code"),
"status": response.status_code
}
}
except Exception as e:
return {
"success": False,
"error": {
"message": str(e),
"status": 0
}
}
# Usage example
client = AgentClient("your mnemonic phrase here", "http://localhost:3000")
result = client.call("hello", data={"name": "Alice"})

HTTP Request Format

Headers:

Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json

Request Body (POST/PUT/PATCH):

{
"field1": "value1",
"field2": "value2"
}

Response Handling

Success Response (200):

{
"result": "success",
"data": { ... }
}

Error Responses:

{
"message": "Error description",
"code": "ERROR_CODE"
}

SR25519 Key Generation

To generate keypairs from mnemonic phrases, you’ll need SR25519 cryptographic libraries:

  • Python: sr25519-python or substrate-interface
  • Go: go-sr25519 or go-substrate-crypto
  • Rust: sr25519 or sp-core
  • JavaScript: @polkadot/util-crypto

Error Codes

Common error codes you should handle:

  • MISSING_AUTH_HEADERS: No Authorization header provided
  • INVALID_JWT: JWT token is malformed or invalid
  • JWT_EXPIRED: JWT token has expired
  • INVALID_SIGNATURE: SR25519 signature verification failed
  • NAMESPACE_ACCESS_DENIED: Insufficient permissions for endpoint
  • INVALID_INPUT: Request data validation failed

Best Practices

Security

  • Store mnemonics securely using environment variables
  • Use secure key management systems in production
  • Implement proper error handling to avoid information leakage
  • Validate responses before using the data

Performance

  • Reuse AgentClient instances when possible
  • Implement connection pooling for high-throughput scenarios
  • Use appropriate timeouts for network requests
  • Consider caching for frequently accessed data

Error Handling

  • Always check the success field before accessing data
  • Implement retry logic for transient failures
  • Log errors appropriately for debugging
  • Provide meaningful error messages to users

Troubleshooting

Common Issues

Authentication Failures

// Check if your mnemonic is correct
const keyInfo = await keypair.getKeyInfo();
console.log("Using address:", keyInfo.address);

Network Errors

// Verify the base URL is correct
console.log("Connecting to:", client.baseUrl);

Permission Errors

// Check if you have the required capability permissions
if (response.error?.code === "NAMESPACE_ACCESS_DENIED") {
console.log("You need permission for this namespace");
}