For engineers · pricing reference

Pricing, metering, on-chain payment

How a capability declares its price, how usage gets metered, how the Arbitrum payment ticket flows. The wire format and the human-readable format are separate fields — knowing why is the load-bearing part of this doc.

Mental model in three sentences

Each capability declares two pricing fields: price_per_unit (wei, the on-chain wire price the orch verifies tickets against) and display_price_usd (the human-readable rate the cost-preview surfaces). They must agree.

Every call signs a payment ticket on Arbitrum, sized to price_per_unit × units. The orch verifies the ticket before running the work; the worker container completes the work; the orch redeems the ticket on-chain (typically batched).

Metering is per-capability and follows unit_kindcall for tools / TTS, megapixel for images, second for video, 1k-chars for text. The adapter counts the units after the work completes and bills accordingly.

The MCP layer enforces a soft per-bearer 24h spend cap (default $50) via the spend_cap tool — see §10. Hard limit is still the wallet deposit; the cap is a UX speed bump for runaway agent loops and stolen keys.

§1Anatomy of a capability row

This is the canonical shape. Wire-protocol fields (orch sees them) on the left, display-only fields (surfaced via /capabilities) on the right.

{
  // ─── identity ───────────────────────────────────────────
  "name": "flux-dev",                    // stable id
  "kind": "ai",                          // "ai" | "tool" | "mcp"

  // ─── routing ────────────────────────────────────────────
  "container": "serverless-proxy",
  "endpoint": "/inference",
  "capacity": 4,                         // max concurrent jobs

  // ─── wire pricing (orch verifies tickets against these) ─
  "price_per_unit": 800000000000,        // wei per unit
  "price_scaling": 1,                    // scaling factor (default 1 since 2026-05)
  "pixels_per_unit": 1000000,            // for unit_kind=megapixel

  // ─── display pricing (humans + cost preview see these) ─
  "unit_kind": "megapixel",             // "call" | "megapixel" | "second" | "1k-chars"
  "display_unit": "megapixel",
  "display_price_usd": 0.026,            // what the marketer sees

  // ─── provenance (optional, used by /capabilities + UI) ─
  "upstream_provider": "fal-ai",
  "upstream_cost_usd": 0.022,            // what we pay fal
  "margin_pct": 18.2,                    // derived markup
  "tier": "image_premium"
}

Source: ToolCapability dataclass in simple-infra/tool-host-build/tool-adapter/src/livepeer_tool_adapter/config.py. Same shape on the BYOC inference-adapter.

§2The wire vs display split — why two prices?

Two reasons.

(1) The orch verifies tickets in wei, not USD. The on-chain signer doesn't know about USD. It signs a payment ticket sized in wei. The orch refuses the work if the ticket's wei value is below price_per_unit × units. So price_per_unit has to be wei.

(2) USD wobbles, wei doesn't. If you tied wire prices to USD, every ETH price tick would invalidate all your published rates. The wire price stays put; the display price reflects current conversion. They're independently editable.

The relationship — when in doubt

# For unit_kind=megapixel:
display_price_usd = (price_per_unit / 1e18) * eth_usd_rate * megapixels_per_unit_normalization

# Worked example for flux-dev at the time of writing:
price_per_unit     = 800_000_000_000        # 8e11 wei per pixel
pixels_per_unit    = 1_000_000              # 1 megapixel = 1M pixels
# wei per megapixel = 8e11 * 1e6 = 8e17 wei
# at ETH ≈ $3,250: 8e17 wei * 3250 / 1e18 ≈ $0.026 per megapixel  ✓
display_price_usd  = 0.026

What can go wrong

"Insufficient balance for request" on every inference. Usually means the wire price_per_unit jumped without the bearer wallet topping up. The signer can't fund tickets at the new rate. Either lower the wire price or top up the wallet. The display price is irrelevant to this error.
display_price_usd shows $0.00. Forgot to set it on the capability row. Wire price is fine; the cost-preview / agent comparison won't work. Fix: add the field, restart the adapter (down && up -d, not restart).

§3Unit kinds — what gets counted

unit_kindWhat counts as one unitTypical capsDisplay unit
callOne invocation, regardless of input sizeffmpeg-*, opencv-*, pillow-*, obscura-*, mcp-bridgedper call
megapixelOne output megapixelflux-dev, gpt-image, recraft-v4, kontext-edit, topaz-upscaleper megapixel
secondOne second of generated videoveo-t2v, ltx-i2v, seedance-i2v, pixverse-i2vper second
1k-chars1,000 characters of TTS audio outputchatterbox-tts, inworld-tts, gemini-ttsper 1k chars

Why megapixel rather than a flat per-call image price: a 512×512 image is 0.26 MP; a 1024×1024 is 1.05 MP; a 2048×2048 is 4.2 MP. Fal charges roughly 4× for the 4× pixels. If we billed per call, the small-image user subsidizes the large-image user. Per-megapixel keeps it honest.

Why second for video: 5s clip = 5 units, 30s clip = 30 units. Roughly tracks the inference cost on fal / Replicate.

Why 1k-chars for TTS: matches how OpenAI / ElevenLabs / Voxtral bill their TTS. A 1000-character paragraph synthesized at one rate.

§4End-to-end payment flow — one inference call

  1. Agent submits work
    Browser / MCP-client sends an /inference request to the SDK service. Request carries the bearer (the user's Daydream API key) in the Authorization header.
  2. SDK service asks the signer for a payment ticket
    SDK posts to signer.daydream.live/generate-live-payment with the bearer. The signer looks up the user's wallet on Arbitrum, signs a ticket sized to capability.price_per_unit × estimated_units, returns the signed ticket.
  3. SDK forwards to the BYOC orch with the ticket attached
    go-livepeer orch on byoc-staging-1.daydream.monster:8935 receives /inference + ticket + capability name.
  4. Orch verifies the ticket BEFORE doing work
    Orch validates: ticket signature is valid, signer is the expected wallet, ticket face value ≥ price_per_unit × estimated_units. If ticket is short, return 402 / "Insufficient balance" — work never runs.
  5. Orch routes to the adapter
    The inference-adapter (for ai/tool) or tool-adapter (for tool/mcp) looks up the capability row, dispatches to the right container + endpoint. For kind: ai, the serverless-proxy translates the envelope into the vendor's API call.
  6. Work executes upstream
    fal / Gemini / OpenAI / Replicate / etc. runs the model. Or a Docker tool container runs ffmpeg. Or the MCP bridge calls Slack. Each returns its native response.
  7. Adapter normalizes the response + counts the units
    The output URL is returned. The adapter now knows the actual unit count — pixels rendered, seconds generated, characters synthesized, or just 1 for per-call. This is the metering moment. The response envelope carries cost_paid_wei, units_metered, and unit_kind so downstream can reconcile the on-chain charge against the display USD without trusting a separate ledger.
  8. Orch redeems the ticket on-chain
    Typically batched (the orch accumulates tickets and submits a single Arbitrum transaction periodically). The orchestrator operator receives the wei value; gas comes off the ticket value. The user never sees this.
  9. SDK + agent see the response, cost is logged
    Storyboard's get_cost_report MCP tool surfaces the per-call USD value (from display_price_usd × actual_units) so the marketer / engineer can see their spend retrospectively. Source of truth is the on-chain receipts; the display layer is a convenience.

§5Where pricing lives — the files

ConcernWhere it's declared
Wire price_per_unitCAPABILITIES_JSON in /opt/byoc/.env on byoc-staging-1 (and tool-staging-1 for tools)
Display display_price_usdSame file. Add explicitly per cap. Backwards-compat default is 0.0.
Unit kind + pixels_per_unitSame file.
Per-bearer wallet balanceStored on Arbitrum; the signer reads it.
Cost-preview surfacing in storyboardstoryboard-a3/lib/sdk/capabilities.ts +scripts/scorecard.ts
MCP tool get_pricingstoryboard-a3/lib/mcp-server/tools/get-pricing.ts — reads from /capabilities, returns USD rates
MCP tool get_cost_reportstoryboard-a3/lib/mcp-server/tools/get-cost-report.ts — aggregates retrospective spend from per-bearer job log
Per-job cost estimateEmbedded in the create_media response; surfaced on the canvas card

§6The cost-preview path — what a user actually sees

Before any inference fires, storyboard estimates the cost and surfaces it. The flow:

// In lib/mcp-server/tools/moodboard-spread.ts (sample):
const caps = await getCachedCapabilities();
const cap = caps.find(c => c.name === "flux-dev");
const mpx = (1024 * 1024) / 1_000_000;                    // = 1.05 MP per image
const estimatedUsd = cap.display_price_usd * mpx;         // $0.026 * 1.05 = $0.027
const twelvePack = estimatedUsd * 12;                     // $0.33 for 12 slots

if (twelvePack > input.max_cost_usd) {
  return error(`12-slot board would cost $${twelvePack.toFixed(2)} — over your $${input.max_cost_usd} cap.`);
}

The agent's max_cost_usd argument (on every fan-out tool like moodboard_spread, place_subject, generate_project) is the user's spend safety net. Compute the estimate up-front, reject the call if it'd blow the cap. Source of truth for the estimate is display_price_usd; the on-chain billing is in wei.

§7Retrospective spend — get_cost_report

After work runs, the agent / user can query spend with the get_cost_report MCP tool. The bearer is scoped to one wallet's worth of jobs:

{
  "window": "24h",
  "group_by": "capability"
}

Returns:

{
  "total_usd": 4.23,
  "by_capability": [
    { "name": "flux-dev",       "calls": 47, "usd": 1.22 },
    { "name": "gpt-image",      "calls": 12, "usd": 0.89 },
    { "name": "seedance-i2v",   "calls": 6,  "usd": 1.84 },
    { "name": "ffmpeg-concat",  "calls": 18, "usd": 0.04 },
    { "name": "chatterbox-tts", "calls": 22, "usd": 0.24 }
  ]
}

Numbers come from display_price_usd × actual_units recorded at job-completion time. The on-chain ledger has the true wei amounts; get_cost_report is a USD convenience.

§8On-chain mechanics — Arbitrum specifics

ConcernHow it works
Wallet per userOne Arbitrum wallet per Daydream API key. Top up at app.daydream.live via Stripe → on-chain deposit, or wallet bridge.
Signer servicesigner.daydream.live — signs payment tickets on the user's behalf. The signer is custodial today; the trust model is "you trust the signer not to over-sign." See PR-1..PR-14 in the byoc-payment-fleet effort.
Ticket shapeEach ticket carries: signer address, recipient (orch) address, face value (wei), random salt, signature. Standard Livepeer probabilistic micropayment ticket — same primitive as the live-transcoding network used since 2019.
VerificationOrch verifies signature + face value + recency before doing work. Wrong wallet, expired, or undersized = 402.
RedemptionBatched. Orch accumulates winning tickets and submits one Arbitrum tx periodically. Gas comes off ticket value.
TopupStripe → bridge to Arbitrum → balance shows in app.daydream.live/billing. Minutes, not days.
Withdrawal (orch operators)Standard Arbitrum withdrawal from the orchestrator wallet. No custodial gate.

§9What this isn't

Honest disclaimers — what the system doesn't do today:

§10Cost guardrails — the 2026-05-21 audit fixes

A pricing audit on 2026-05-21 surfaced eight fixable issues. All eight shipped on livepeer/storyboard PR #291 (MCP-side) and livepeer/simple-infra PR #50 (adapter-side). Here's what changed and why each matters.

Per-bearer 24h soft spend cap — spend_cap tool (H3)

The orch is wallet-balance-or-bust. Before this audit, a runaway agent loop on a stolen key could drain a deposit in minutes. The fix is a soft cap stored per-bearer in Vercel Blob, checked by create_media before ticket submission:

// Read current spend + cap + utilization:
spend_cap({action: "read"})
// → {cap_usd: 50, spent_usd: 7.50, count_24h: 14, utilization_pct: 15, remaining_usd: 42.50}

// Override the cap (max $10,000):
spend_cap({action: "set", cap_usd: 100})

// Revert to the default ($50):
spend_cap({action: "reset"})

When the next create_media would push 24h spend over the cap, the call returns spend_cap_exceeded with the current spend + remaining budget. The user / agent can raise the cap (intentional) or stop (correctly contained). Hard limit is still the wallet deposit; this is a UX speed bump in front of the wallet limit.

Idempotency replay surfaces cost drift — replay_cost_drift (M1)

Replaying a cached job (via idempotency_key) returns the original job's URL — but if the capability's display_price_usd changed between original and replay, the cached cost is stale. The replay envelope now carries a replay_cost_drift object when the new estimate diverges from the cached one by ≥ $0.001:

{
  "replayed_from": "cached-job-abc",
  "cost_usd_estimated": 0.027,             // what we charged then
  "replay_cost_drift": {
    "cached_usd": 0.027,
    "current_usd": 0.052,                   // what the same call would cost now
    "delta_usd": 0.025,
    "note": "replayed from cache; current rate differs"
  }
}

Agents can show "this was billed at the old rate" instead of silently presenting a stale number as current. Below the $0.001 threshold the field is omitted — floating-point chatter doesn't deserve a flag.

Cache-bypass on get_pricingbypass_cache (M4)

SDK /capabilities has a 60s TTL cache (it talks to the BYOC orch on miss). On a fresh price deploy, the agent might see stale rates for up to a minute. The MCP tool now accepts bypass_cache: true which forwards ?bypass_cache=true to SDK and forces a re-fetch. Default is unset — the cache is fine 99% of the time. Use this only when verifying a just-deployed price change.

Pricing-drift validator on the adapter (M2)

If a capability declares all of upstream_cost_usd, margin_pct, and display_price_usd, the adapter computes upstream × (1 + margin/100) at registration and logs WARN if the derived USD diverges from the display USD by >5%. The intent: catch an operator who silently bumped display_price_usd without updating upstream tracking. It never blocks registration — many caps don't declare upstream tracking at all, and that's still valid.

HTTPS enforcement on mcp_server (M3)

kind: mcp capabilities relay the caller's bearer token to the upstream MCP server. If the upstream URL is plaintext http://, that token goes over the wire in the clear. Adapter now rejects http:// mcp_server URLs at registration with a ValueError — too late to discover in production.

Cost accumulator IEEE 754 rounding (L1)

get_cost_report sums per-job USD costs in a loop. JavaScript's 0.1 + 0.2 = 0.30000000000000004; over 100 small jobs this drifts visibly. Each accumulator add is now Math.round((sum + delta) * 10000) / 10000 — clean to four decimals, the granularity the display layer uses anyway.

Cross-surface kind consistency test (L2)

A regression test now reads FALLBACK_CAPABILITIES + CAPABILITY_KIND from lib/sdk/capabilities.ts and confirms every capability declared on either side has a kind mapping on the other. Caught real false-positive misses during landing — comments inside the fallback block were tripping the regex; the test strips line comments before parsing.

DEFAULT_PRICE_SCALING note (L3)

DEFAULT_PRICE_SCALING = 1_000_000 stays the back-compat default but is now annotated in config.py. Production capabilities should set price_scaling: 1 explicitly. The default is load-bearing for old rows; a future cleanup pass could lower it to 1 once every cap row has been migrated.

H2 in the original audit was wrong. The audit claimed costPaidUsdFromWei was dead code. Verification showed it's called from create-media.ts:858 and get-lora-train.ts:158. Withdrew the finding. The actual gap was upstream: the adapter wasn't emitting cost_paid_wei for the reconciler to read. H1 closes that.

§11Practical recipes

Add a new capability with explicit USD pricing

// Add to CAPABILITIES_JSON on byoc-staging-1:
{
  "name": "my-new-cap",
  "container": "serverless-proxy",
  "endpoint": "/inference",
  "capacity": 2,
  "kind": "ai",
  "price_per_unit": 1500000000000,    // 1.5e12 wei per pixel
  "pixels_per_unit": 1000000,
  "unit_kind": "megapixel",
  "display_unit": "megapixel",
  "display_price_usd": 0.050,           // MUST match the wire calc
  "upstream_provider": "fal-ai",
  "model_id": "fal-ai/some-model"
}

Change a published price safely

  1. Compute the new wire price_per_unit from the desired USD (see §2).
  2. Verify your bearer wallets carry enough deposit at the new rate. Otherwise existing users see "Insufficient balance" on next call.
  3. Edit CAPABILITIES_JSON (Python, not sed).
  4. Restart with docker compose down && docker compose up -d (not restart).
  5. Confirm in /capabilities and via the MCP get_pricing tool.

Spot-check a metering issue

# Pull the per-call billing event from the adapter logs:
sudo docker logs byoc-adapter --tail 200 | grep -i billing | tail -10

# Cross-reference with the agent's get_cost_report output:
curl -X POST https://storyboard.daydream.monster/api/mcp \
  -H "Authorization: Bearer $KEY" \
  -d '{"method":"tools/call","params":{"name":"get_cost_report","arguments":{"window":"1h"}}}' \
  | python3 -m json.tool

§12Reading list