Skip to main content

Overview

Caisper includes 2 background services that run continuously while your ElizaOS agent is active:

PayAI Polling Service

Monitors blockchain for x402 payments and updates reputation automatically

Starter Service

Template service for custom GhostSpeak integrations
Services are long-running background processes. Unlike Actions (handle user commands) and Providers (supply context data), services perform continuous operations.

PayAI Polling Service

Purpose

The PayAI Polling Service monitors Solana blockchain for x402 payment protocol transactions and automatically updates agent reputation scores.

How It Works

Configuration

pollInterval
number
default:300000
Polling interval in milliseconds (default: 5 minutes)
// Minimum: 60000 (1 minute)
// Default: 300000 (5 minutes)
// Maximum: 3600000 (1 hour)
batchSize
number
default:10
Number of transactions to fetch per poll
// Minimum: 5
// Default: 10
// Maximum: 100
enabled
boolean
default:true
Enable/disable service at runtime

Service Lifecycle

1

Initialization

Service starts when ElizaOS agent loads Caisper plugin:
// Automatic startup
await PayAIPollingService.start(runtime)

// Service initialized
logger.info('PayAI Polling Service started')
2

Polling Loop

Every 5 minutes (configurable):
// 1. Fetch all registered agents
const agents = await client.agents.getAllAgents()

// 2. Check payments for each agent
for (const agent of agents) {
  await checkAgentPayments(agent.address)
}

// 3. Process new payments
await processPayment(paymentRecord)
3

Payment Processing

For each payment transaction:
// Extract payment details
const payment = {
  signature: "5jHD8z9x...",
  agentAddress: address("7xKXt..."),
  amount: 1_000_000n, // lamports
  payer: address("3zYx..."),
  timestamp: 1234567890,
}

// Update reputation (future feature)
// await updateAgentReputation(payment)

// Mark as processed
processedSignatures.add(signature)
4

Shutdown

Clean shutdown when agent stops:
// Stop polling
clearInterval(intervalId)

// Clear cache
processedSignatures.clear()

logger.info('PayAI Polling Service stopped')

Runtime Control

import type { IAgentRuntime } from '@elizaos/core'
import { PayAIPollingService } from '@ghostspeak/plugin-elizaos'

// In action or custom code
const service = runtime.getService<PayAIPollingService>('payai-polling')

if (service) {
  const stats = service.getStats()
  console.log('Processed payments:', stats.processedPayments)
}

Error Handling

The service includes robust error handling:
Symptom: Failed to connect to RPCHandling:
try {
  const signatures = await rpc.getSignaturesForAddress(agentAddress).send()
} catch (error) {
  logger.error({ error, agentAddress }, 'Failed to fetch signatures')
  // Service continues, will retry next poll cycle
}
Service logs error and continues polling (doesn’t crash).
Symptom: Failed to parse transactionHandling:
if (!transaction || transaction.meta?.err) {
  // Skip failed transaction
  continue
}
Failed transactions are skipped, not reprocessed.
Symptom: 429 Too Many RequestsHandling:
  • Service uses 5-minute intervals by default (well below rate limits)
  • Batch size limited to 10 transactions per agent
  • Use premium RPC endpoint for production
# Use Helius/QuickNode for higher limits
SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY

Monitoring

Enable debug logging to monitor service activity:
# Start with debug logs
LOG_LEVEL=debug elizaos dev

# Watch for polling activity:
# [PayAI] Polling for new payments...
# [PayAI] Checking payments for 15 agents
# [PayAI] Found 3 new payments
# [PayAI] Processing payment: 5jHD8z9x...
# [PayAI] Payment polling complete

API Endpoints

The service exposes monitoring endpoints:
curl http://localhost:3000/api/payai/stats

Starter Service

Purpose

Template service for custom GhostSpeak integrations. Demonstrates service structure and lifecycle.

Usage

import { StarterService } from '@ghostspeak/plugin-elizaos'

// Service auto-starts with plugin
// Access via runtime
const service = runtime.getService<StarterService>('starter')

console.log(service.capabilityDescription)
// "This is a starter service which is attached to the agent through the starter plugin."

Extend for Custom Services

Create your own GhostSpeak services:
import { Service, logger } from '@elizaos/core'
import type { IAgentRuntime } from '@elizaos/core'
import { GhostSpeakClient } from '@ghostspeak/sdk'

export class CustomGhostSpeakService extends Service {
  static serviceType = 'custom-ghostspeak'

  capabilityDescription = 'Custom GhostSpeak integration service'

  private client: GhostSpeakClient
  private intervalId: NodeJS.Timeout | null = null

  constructor(protected runtime: IAgentRuntime) {
    super(runtime)

    this.client = new GhostSpeakClient({
      cluster: process.env.SOLANA_CLUSTER as any || 'devnet',
      rpcEndpoint: process.env.SOLANA_RPC_URL,
    })
  }

  static async start(runtime: IAgentRuntime) {
    logger.info('Starting Custom GhostSpeak Service')
    const service = new CustomGhostSpeakService(runtime)
    await service.initialize()
    return service
  }

  static async stop(runtime: IAgentRuntime) {
    const service = runtime.getService(CustomGhostSpeakService.serviceType)
    if (service) {
      await (service as CustomGhostSpeakService).shutdown()
    }
  }

  async initialize() {
    // Your initialization logic
    logger.info('Custom GhostSpeak Service initialized')

    // Start background task
    this.intervalId = setInterval(() => {
      this.performTask()
    }, 60000) // Every minute
  }

  async stop() {
    await this.shutdown()
  }

  private async shutdown() {
    if (this.intervalId) {
      clearInterval(this.intervalId)
      this.intervalId = null
    }
    logger.info('Custom GhostSpeak Service stopped')
  }

  private async performTask() {
    try {
      // Your custom GhostSpeak logic
      const agents = await this.client.agents.getAllAgents()
      logger.debug(`Found ${agents.length} agents`)

      // Example: Update agent metadata
      // Example: Sync with external system
      // Example: Generate reports
    } catch (error) {
      logger.error({ error }, 'Task failed')
    }
  }
}
Register custom service:
import { starterPlugin } from '@ghostspeak/plugin-elizaos'
import { CustomGhostSpeakService } from './services/CustomGhostSpeakService'

const agent = await createAgent({
  plugins: [
    {
      ...starterPlugin,
      services: [...starterPlugin.services, CustomGhostSpeakService],
    },
  ],
})

Service Best Practices

1. Resource Management

export class MyService extends Service {
  private client: GhostSpeakClient | null = null
  private intervalId: NodeJS.Timeout | null = null

  async initialize() {
    this.client = new GhostSpeakClient({ cluster: 'devnet' })
    this.intervalId = setInterval(() => this.task(), 60000)
  }

  async shutdown() {
    // Clean up resources!
    if (this.intervalId) {
      clearInterval(this.intervalId)
      this.intervalId = null
    }
    this.client = null
  }
}
Always clean up resources in shutdown(). Leaked intervals/connections can cause memory issues.

2. Error Recovery

private async performTask() {
  try {
    await this.client.someOperation()
  } catch (error) {
    logger.error({ error }, 'Task failed')
    // DON'T crash the service!
    // Continue to next iteration
  }
}

3. Graceful Degradation

async initialize() {
  try {
    this.client = new GhostSpeakClient({ cluster: 'devnet' })
    logger.info('Service initialized successfully')
  } catch (error) {
    logger.warn({ error }, 'Service initialization failed')
    // Service continues in degraded mode
    this.client = null
  }
}

private async performTask() {
  if (!this.client) {
    logger.debug('Service not initialized, skipping task')
    return
  }
  // Perform task...
}

4. Monitoring & Observability

export class MonitoredService extends Service {
  private metrics = {
    tasksRun: 0,
    errors: 0,
    lastRunTime: 0,
  }

  getStats() {
    return {
      ...this.metrics,
      uptime: Date.now() - this.startTime,
      isHealthy: this.metrics.errors < 10,
    }
  }

  private async performTask() {
    const startTime = Date.now()
    try {
      await this.doWork()
      this.metrics.tasksRun++
    } catch (error) {
      this.metrics.errors++
      logger.error({ error }, 'Task failed')
    } finally {
      this.metrics.lastRunTime = Date.now() - startTime
    }
  }
}

5. Configuration Validation

import { z } from 'zod'

const serviceConfigSchema = z.object({
  pollInterval: z.number().min(60000).max(3600000),
  batchSize: z.number().min(1).max(100),
  enabled: z.boolean(),
})

export class ConfiguredService extends Service {
  private config: z.infer<typeof serviceConfigSchema>

  async initialize() {
    // Validate config
    this.config = serviceConfigSchema.parse({
      pollInterval: Number(process.env.POLL_INTERVAL) || 300000,
      batchSize: Number(process.env.BATCH_SIZE) || 10,
      enabled: process.env.SERVICE_ENABLED !== 'false',
    })

    logger.info({ config: this.config }, 'Service configured')
  }
}

Service Communication

Services can communicate via runtime state:
// Service A writes data
export class ProducerService extends Service {
  async performTask() {
    const data = await this.fetchData()
    await this.runtime.setState('ghostspeak:data', data)
  }
}

// Service B reads data
export class ConsumerService extends Service {
  async performTask() {
    const data = await this.runtime.getState('ghostspeak:data')
    if (data) {
      await this.processData(data)
    }
  }
}
Use namespaced keys (e.g., ghostspeak:*) to avoid conflicts with other plugins.

Debugging Services

Enable Service Logs

# Debug logs
LOG_LEVEL=debug elizaos dev

# Watch specific service
LOG_LEVEL=debug elizaos dev 2>&1 | grep "PayAI"

Inspect Service State

// In ElizaOS console or custom action
const service = runtime.getService('payai-polling')

console.log('Service type:', service.serviceType)
console.log('Capability:', service.capabilityDescription)
console.log('Stats:', service.getStats())

Health Check Endpoint

Create a health check route:
{
  name: 'service-health',
  path: '/api/services/health',
  type: 'GET',
  handler: async (req, res) => {
    const payaiService = runtime.getService('payai-polling')
    const starterService = runtime.getService('starter')

    res.json({
      services: {
        'payai-polling': payaiService ? 'active' : 'inactive',
        'starter': starterService ? 'active' : 'inactive',
      },
      stats: {
        payai: payaiService?.getStats() || null,
      }
    })
  }
}
Test:
curl http://localhost:3000/api/services/health

Next Steps