Sign in →
1 min read

Agentic APIs & MCP Server Monetization

Monetizing the Agentic Economy. Bill for tool calls, reasoning tokens, and long-running agent sessions in real time.

Updated 2026-11-11Suggest edits

Monetizing the Agentic Economy#

Traditional billing counts API requests. The agentic era demands more. A single agent session may invoke dozens of tools, consume thousands of reasoning tokens, and run for minutes or hours. Aforo is the first monetization platform built for this reality.

Tool-Call Metering
Bill per successful tool execution, not raw HTTP requests
Token Aggregation
Pass through LLM costs (input + output + reasoning) in real time
Session Billing
Orchestrate access and billing for long-running agent threads
INFO
Aforo treats MCP Servers as a first-class product type — alongside Standard API, Agentic API, and AI Agent. Each has its own metering schema, billing hierarchy, and credential type.

Tool-Call Metering#

Instead of billing per HTTP request, Aforo meters at the tool execution level. When an AI agent calls tools/call via JSON-RPC, the Aforo gateway plugin (or SDK decorator) extracts the tool name, execution status, and duration:

tool-call-event.json
{
  "customerId": "cus_123",
  "metricName": "tool_invocations",
  "quantity": 1,
  "occurredAt": "2026-07-01T10:00:00Z",
  "idempotencyKey": "call_9c1e",
  "productType": "MCP_SERVER",
  "toolName": "SmartSearch.query",
  "agentId": "agent_uuid_456",
  "sessionId": "session_uuid_789",
  "executionStatus": "SUCCESS",
  "executionDurationMs": 342
}

This enables per-tool pricing. Charge $0.001 for a simple lookup, $0.05 for a database write, and $0.10 for an AI-powered analysis — all within the same MCP server subscription.

Reasoning-Token Aggregation#

For AI-powered tools that call LLMs internally, Aforo captures the token breakdown and maps it directly to your COGS:

token-event.json
{
  "customerId": "cust_abc123",
  "metricName": "ai_tokens",
  "quantity": 3200,
  "occurredAt": "2026-09-30T12:00:00Z",
  "idempotencyKey": "call_7f3a9c:tokens",
  "productType": "MCP_SERVER",
  "toolName": "analyze_document",
  "agentId": "agent_claude_001",
  "metadata": {
    "input_tokens": 2400,
    "output_tokens": 800,
    "provider": "anthropic",
    "model": "claude-sonnet-4-6"
  }
}
INFO
You don't send a cost. Aforo reads provider, model, input_tokens and output_tokens from metadata, prices them with that model's provider rates, and records the result as the event's cost in COGS & Margins. quantity is what the customer is billed for.

Session-Based Billing#

Agent sessions are long-running. A single session may span minutes, invoke 50+ tool calls, and consume thousands of tokens. Aforo tracks the full session lifecycle:

SESSION LIFECYCLE
ACTIVE → First tool_call assigns a session_id; tool calls metered, tokens aggregated, duration tracked
TIMED_OUT → No activity past the idle timeout (default 3,600s / 1 hr) → session closed automatically
COMPLETED → Explicit close → session closed cleanly
FAILED → Session ended in error

TIMED_OUT, COMPLETED, and FAILED are terminal. Each tool call is metered as it happens; the session record consolidates total tool calls, tokens, duration, and provider cost for full COGS attribution.

MCP Server Integration#

Here is how an MCP server integrates with Aforo. The entitlement check runs before every tool execution:

mcp-server-config.json
{
  "mcpServer": {
    "name": "SmartSearch",
    "version": "2.1.0",
    "transport": "stdio",
    "tools": [
      {
        "name": "SmartSearch.query",
        "description": "AI-powered semantic search",
        "monetization": {
          "aforo_product_id": "prod_smartsearch_uuid",
          "aforo_metric_id": "tool_invocations",
          "entitlement_check": true,
          "margin_guard": true,
          "pricing_tier": "standard"
        }
      },
      {
        "name": "SmartSearch.index",
        "description": "Index documents for search",
        "monetization": {
          "aforo_product_id": "prod_smartsearch_uuid",
          "aforo_metric_id": "tool_invocations",
          "entitlement_check": true,
          "margin_guard": true,
          "pricing_tier": "premium"
        }
      }
    ],
    "aforo": {
      "api_key": "{AFORO_API_KEY}",
      "tenant_id_source": "request.headers.x-tenant-id",
      "session_timeout_ms": 3600000,
      "batch_size": 50,
      "credential_type": "CLIENT_CREDENTIALS"
    }
  }
}

SDK Integration (Decorator Pattern)#

For MCP servers built with our SDKs, use the wrapToolHandler decorator to automatically meter every tool call:

Node.js

mcp-server.js
import { AforoMcpBilling } from '@aforoai/mcp-metering';
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';

const billing = new AforoMcpBilling({
  tenantId: process.env.AFORO_TENANT_ID,
  productId: 'prod_smartsearch_uuid',
  apiKey: process.env.AFORO_API_KEY,
  ingestorUrl: 'https://api.aforo.ai',   // base URL; all four are required
  // Who each call is billed to, from identity your server authenticated
  // (the MCP SDK passes it to the handler as extra.authInfo):
  customerResolver: (request, extra) => extra?.authInfo?.extra?.aforoCustomerId,
});

// Wrap any tool handler — timing and metering are automatic; errors re-throw unchanged
server.setRequestHandler(CallToolRequestSchema, billing.wrapToolHandler(
  async (request, extra) => {
    const { name, arguments: args } = request.params;
    // Your tool logic here
    return { content: [{ type: 'text', text: result }] };
  }
));

// The SDK installs no signal handlers. Send what is queued from your own:
process.once('SIGTERM', async () => { await billing.shutdown(); process.exit(0); });

Python

mcp_server.py
import os
from aforo_mcp_metering import AforoMcpBilling

billing = AforoMcpBilling(
    tenant_id=os.environ["AFORO_TENANT_ID"],
    product_id="prod_smartsearch_uuid",
    api_key=os.environ["AFORO_API_KEY"],
    ingestor_url="https://api.aforo.ai",   # all four are required
    # Who pays for a call — from what your server authenticated,
    # never from the tool arguments the MCP client sends.
    customer_resolver=lambda call: current_session().customer_id,
)

@server.call_tool()
@billing.wrap_tool_handler
async def handle_tool(name: str, arguments: dict, **kwargs):
    # Your tool logic here; kwargs may carry agent_id / session_id
    return [TextContent(type="text", text=result)]

# On shutdown (also runs at interpreter exit):
await billing.shutdown()

Who is billed

From 2.0.0 the decorators need a customer source and fail when you wrap a handler without one (Node throws, Python raises ValueError). A call is never billed to its agent id or to a placeholder such as unknown.

  • customerResolver / customer_resolver — your server authenticates callers (OAuth token, API key, session) and returns the Aforo customer id.
  • customerId (Node) — one fixed customer, for a server that serves one customer.
  • trustCustomerHeader: true / trust_customer_header=True — bill params._meta.customer_id as the client sends it. Any MCP client can put any id there, so use it only when every client is yours or your transport sets the value itself.

When the resolver returns nothing for a call, the tool still runs; its event is dropped as invalid and counted. agentId is read from _meta.agent_id (Python: the agent_id kwarg) and identifies the MCP client, not the payer.

Proxy sidecar (no code change)

When you can't change the MCP server, run @aforoai/mcp-proxy in front of it over stdio, SSE or Streamable HTTP. From 2.0.0 it refuses to start without a customer source (--customer-id, a bearer-token-to-customer file, or the explicit --trust-client-customer-id):

terminal
# stdio: one customer per sidecar. Use --arg once per argument (commas are kept).
aforo-mcp-proxy --transport stdio \
  --command "npx" --arg -y --arg @modelcontextprotocol/server-filesystem --arg /tmp \
  --tenant tenant_smartai --product prod_mcp_fs --customer-id cust_acme \
  --api-key "$AFORO_API_KEY" --ingestor-url https://api.aforo.ai

# SSE / Streamable HTTP: every client sends a bearer token; the file maps token -> customer
aforo-mcp-proxy --transport sse \
  --upstream http://localhost:8080/sse --port 3100 --host 127.0.0.1 \
  --auth-tokens-file /etc/aforo/proxy-tokens.json \
  --tenant tenant_smartai --product prod_mcp_fs \
  --api-key "$AFORO_API_KEY" --ingestor-url https://api.aforo.ai
  • The HTTP listeners need --auth-token (at least 16 characters), --auth-tokens-file, or --allow-unauthenticated on a loopback --host. The token is never forwarded to the MCP server.
  • Requests with an unexpected Host get 421 and with an unlisted Origin get 403 (add them with --allowed-hosts / --allowed-origins); bodies over 4 MiB get 413.
  • The stdio child inherits only PATH, HOME, locale, temp and proxy variables. Name anything else it needs with --pass-env; AFORO_* variables are never passed.
  • With --quota-enforcement, each check has a 1 s deadline and fails open by default; --quota-fail-mode closed refuses calls the check cannot answer (JSON-RPC -32000, QUOTA_CHECK_UNAVAILABLE).

What each tool call is billed as

The decorators and the @aforoai/mcp-proxy sidecar set executionStatus from what the tool did. Only Outcome-Based rate plans read it; every other model bills the call at its normal rate.

What happenedexecutionStatus
Handler returned a resultSUCCESS
Handler returned a result with isError: trueERROR
Handler threw a timeout (TimeoutError, or an MCP error with code -32001)TIMEOUT
Handler threw anything elseERROR
Python only: task cancelled or interrupted (asyncio.CancelledError, KeyboardInterrupt)CANCELLED
Proxy only: still waiting when the proxy shuts down (SIGTERM/SIGINT)CANCELLED
Proxy only: no response within AFORO_RESPONSE_TIMEOUT_MS (default 300000 — 5 minutes)TIMEOUT
WARNING
Override the result with a status resolver (statusResolver in Node and the proxy, status_resolver in Python). It must be synchronous and return one of the 11 statuses (SUCCESS, PARTIAL, TIMEOUT, ERROR, VALIDATION_FAILED, FAILED, FAILURE, CANCELLED, PENDING, BLOCKED, HITL_REQUIRED). A Promise or any other value is logged and the derived status is sent instead — never an unknown string, which the ingestor would reject, losing that tool call's usage. Raise AFORO_RESPONSE_TIMEOUT_MS for tools that legitimately run longer than 5 minutes: a response that arrives after the timeout is not metered again.

Billing Hierarchy#

MCP Server billing follows the same hierarchy as all Aforo product types:

Customer → Team → Agent → API Key
Usage tracked at Session level. Keys assigned at Agent level. Credential type: CLIENT_CREDENTIALS.
Product TypeHierarchyUsage LevelCredential
Standard APICustomer → Team → AppApp InstanceBEARER_TOKEN
Agentic APICustomer → Team → AppApp InstanceBEARER_TOKEN
AI AgentCustomer → Team → AgentAgent SessionCLIENT_CREDENTIALS
MCP ServerCustomer → Team → AgentSessionCLIENT_CREDENTIALS
PRO TIP
MCP Server is the 4th GA product type in Aforo. It uses the same pricing engine, billing pipeline, and storefront infrastructure as Standard APIs — but with session-aware metering and tool-level pricing granularity built in.

What this guide does NOT cover#

This page walks the on-the-wire side of MCP metering — how a tools/call becomes a billable event, how sessions are tracked, and how the gateway plugins + SDK decorators emit that data. It does not cover:

  • Creating an MCP Server product in your catalog, wiring the tool registry, and choosing transport/integration mode — see MCP Server (Catalog).
  • Pricing model choices per tool (per-tool multipliers via dimensionPricing) — same page.
  • Which billable units to attach to an MCP Server product — see Billable Units.
  • End-customer registration of agents and rotation of client credentials — see Customers.
  • Operator-side MCP analytics dashboards. Those live inside the Aforo product console, not on this docs site.
Aforo Documentation — Aforo Docs