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
npm install @torus-network/torus-ts-sdk
Basic Usage
import { AgentClient, Keypair } from "@torus-network/torus-ts-sdk";
// Create a keypair from your mnemonicconst keypair = new Keypair( "your twelve word mnemonic phrase goes here exactly like this");
// Create the clientconst client = new AgentClient({ keypair, baseUrl: "http://localhost:3000",});
// Make a call to the agentconst 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 mnemonicconst keypair = new Keypair( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about");
// Get key informationconst 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 dataconst 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);}
// GET requestconst response = await client.call({ endpoint: "get-memory", method: "GET", data: { id: "memory-123" }});
if (response.success) { console.log("Memory content:", response.data.content);}
// PUT request for full replacementconst response = await client.call({ endpoint: "item", method: "PUT", data: { id: "memory-123", content: "Complete new memory content", tags: ["updated", "important"], metadata: { type: "note", priority: "high" } // Full resource replacement }});
// PATCH request for partial updatesconst response = await client.call({ endpoint: "item", method: "PATCH", data: { id: "memory-123", updates: { content: "Updated memory content", tags: ["updated", "important"] } // Only updating specific fields }});
// DELETE requestconst response = await client.call({ endpoint: "delete-memory", method: "DELETE", data: { id: "memory-123" }});
if (response.success) {console.log("Memory deleted successfully");}
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 callconst 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", }; } }}
// Usageconst memoryClient = new MemoryAgentClient( "your mnemonic phrase here", "http://localhost:3000");
// Store a memoryconst 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 memoriesconst 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 configurationconst 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 logicasync 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 retryconst response = await callWithRetry({ endpoint: "important-operation", data: { critical: "data" },});
Authentication Details
The AgentClient automatically handles JWT authentication:
- JWT Creation: Creates a JWT token using your keypair’s SR25519 signature
- Token Management: Tokens are created fresh for each request
- Protocol Metadata: Includes protocol version information for compatibility
- 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
- SR25519 Keypair: Generate from mnemonic seed phrase
- JWT Structure: Header + Payload + Signature
- Protocol Metadata: Version compatibility information
JWT Creation Process
import jsonimport base64import timeimport uuidfrom 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
orsubstrate-interface
- Go:
go-sr25519
orgo-substrate-crypto
- Rust:
sr25519
orsp-core
- JavaScript:
@polkadot/util-crypto
Error Codes
Common error codes you should handle:
MISSING_AUTH_HEADERS
: No Authorization header providedINVALID_JWT
: JWT token is malformed or invalidJWT_EXPIRED
: JWT token has expiredINVALID_SIGNATURE
: SR25519 signature verification failedNAMESPACE_ACCESS_DENIED
: Insufficient permissions for endpointINVALID_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 accessingdata
- 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 correctconst keyInfo = await keypair.getKeyInfo();console.log("Using address:", keyInfo.address);
Network Errors
// Verify the base URL is correctconsole.log("Connecting to:", client.baseUrl);
Permission Errors
// Check if you have the required capability permissionsif (response.error?.code === "NAMESPACE_ACCESS_DENIED") { console.log("You need permission for this namespace");}