OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / core / incentive / metering / index.js

166 lines · javascript ref:
/**
 * Service Meter
 *
 * Tracks usage per app (by pubkey) across all services.
 * Feeds into PaymentManager for earnings and free-tier for quota enforcement.
 *
 * Usage flows:
 *   1. Router middleware calls meter.record() on every service dispatch
 *   2. Free-tier manager calls meter.getUsage() to check quotas
 *   3. Settlement loop calls meter.flush() to convert usage → earnings
 */

import { EventEmitter } from 'events'

// Default pricing (sats per unit)
const DEFAULT_RATES = {
  'ai.infer': 10, // sats per inference call
  'ai.embed': 5, // sats per embedding call
  'storage.drive-write': 2, // sats per write
  'storage.drive-read': 0, // reads are free
  'identity.sign': 1, // sats per signature
  'identity.verify': 0, // verification is free
  'schema.validate': 0 // validation is free
}

export class ServiceMeter extends EventEmitter {
  constructor (opts = {}) {
    super()
    this.rates = { ...DEFAULT_RATES, ...opts.rates }
    // appPubkey -> { service.method -> { calls, lastCall } }
    this.usage = new Map()
    // Rolling window for quota checks (resets monthly)
    this.windowStart = Date.now()
    this.windowMs = opts.windowMs || 30 * 24 * 60 * 60 * 1000 // 30 days
  }

  /**
   * Record a service call.
   * Called from router middleware on every dispatch.
   */
  record (appPubkey, route, meta = {}) {
    if (!appPubkey) return

    let app = this.usage.get(appPubkey)
    if (!app) {
      app = {}
      this.usage.set(appPubkey, app)
    }

    if (!app[route]) {
      app[route] = { calls: 0, totalCost: 0, lastCall: 0 }
    }

    const entry = app[route]
    entry.calls++
    entry.lastCall = Date.now()

    const rate = this.rates[route] || 0
    entry.totalCost += rate

    this.emit('usage', { app: appPubkey, route, calls: entry.calls, cost: rate })
    return { rate, totalCalls: entry.calls, totalCost: entry.totalCost }
  }

  /**
   * Get usage summary for an app in the current window.
   */
  getUsage (appPubkey) {
    const app = this.usage.get(appPubkey)
    if (!app) return { routes: {}, totalCalls: 0, totalCost: 0 }

    let totalCalls = 0
    let totalCost = 0
    for (const route of Object.keys(app)) {
      totalCalls += app[route].calls
      totalCost += app[route].totalCost
    }

    return { routes: { ...app }, totalCalls, totalCost }
  }

  /**
   * Get total cost for an app (for billing).
   */
  getTotalCost (appPubkey) {
    const usage = this.getUsage(appPubkey)
    return usage.totalCost
  }

  /**
   * Get call count for a specific route (for quota checking).
   */
  getRouteCalls (appPubkey, route) {
    const app = this.usage.get(appPubkey)
    if (!app || !app[route]) return 0
    return app[route].calls
  }

  /**
   * Flush usage into payment earnings and reset counters.
   * Called periodically by settlement loop.
   */
  flush (paymentManager) {
    const results = []

    for (const [appPubkey, routes] of this.usage) {
      let totalCost = 0
      const breakdown = {}

      for (const [route, entry] of Object.entries(routes)) {
        if (entry.totalCost > 0) {
          totalCost += entry.totalCost
          breakdown[route] = { calls: entry.calls, cost: entry.totalCost }
        }
      }

      if (totalCost > 0 && paymentManager) {
        // Record earnings for the relay operator
        try {
          for (const [pubkey] of paymentManager.accounts) {
            paymentManager.recordEarnings(
              pubkey,
              totalCost,
              `Service usage from ${appPubkey.slice(0, 12)}...`
            )
          }
        } catch {}
      }

      results.push({ app: appPubkey, totalCost, breakdown })
    }

    // Reset counters
    this.usage.clear()
    this.windowStart = Date.now()
    this.emit('flushed', { apps: results.length, results })

    return results
  }

  /**
   * Get aggregate stats across all apps.
   */
  stats () {
    let totalApps = 0
    let totalCalls = 0
    let totalRevenue = 0

    for (const [, routes] of this.usage) {
      totalApps++
      for (const entry of Object.values(routes)) {
        totalCalls += entry.calls
        totalRevenue += entry.totalCost
      }
    }

    return {
      totalApps,
      totalCalls,
      totalRevenue,
      windowStart: this.windowStart,
      rates: { ...this.rates }
    }
  }
}