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.
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_kind — call 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.
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.
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.
# 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
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.
down && up -d, not restart).
| unit_kind | What counts as one unit | Typical caps | Display unit |
|---|---|---|---|
call | One invocation, regardless of input size | ffmpeg-*, opencv-*, pillow-*, obscura-*, mcp-bridged | per call |
megapixel | One output megapixel | flux-dev, gpt-image, recraft-v4, kontext-edit, topaz-upscale | per megapixel |
second | One second of generated video | veo-t2v, ltx-i2v, seedance-i2v, pixverse-i2v | per second |
1k-chars | 1,000 characters of TTS audio output | chatterbox-tts, inworld-tts, gemini-tts | per 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.
/inference request to the SDK service. Request carries the bearer (the user's Daydream API key) in the Authorization header.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.byoc-staging-1.daydream.monster:8935 receives /inference + ticket + capability name.price_per_unit × estimated_units. If ticket is short, return 402 / "Insufficient balance" — work never runs.kind: ai, the serverless-proxy translates the envelope into the vendor's API call.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.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.| Concern | Where it's declared |
|---|---|
Wire price_per_unit | CAPABILITIES_JSON in /opt/byoc/.env on byoc-staging-1 (and tool-staging-1 for tools) |
Display display_price_usd | Same file. Add explicitly per cap. Backwards-compat default is 0.0. |
Unit kind + pixels_per_unit | Same file. |
| Per-bearer wallet balance | Stored on Arbitrum; the signer reads it. |
| Cost-preview surfacing in storyboard | storyboard-a3/lib/sdk/capabilities.ts +scripts/scorecard.ts |
MCP tool get_pricing | storyboard-a3/lib/mcp-server/tools/get-pricing.ts — reads from /capabilities, returns USD rates |
MCP tool get_cost_report | storyboard-a3/lib/mcp-server/tools/get-cost-report.ts — aggregates retrospective spend from per-bearer job log |
| Per-job cost estimate | Embedded in the create_media response; surfaced on the canvas card |
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.
get_cost_reportAfter 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.
| Concern | How it works |
|---|---|
| Wallet per user | One Arbitrum wallet per Daydream API key. Top up at app.daydream.live via Stripe → on-chain deposit, or wallet bridge. |
| Signer service | signer.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 shape | Each 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. |
| Verification | Orch verifies signature + face value + recency before doing work. Wrong wallet, expired, or undersized = 402. |
| Redemption | Batched. Orch accumulates winning tickets and submits one Arbitrum tx periodically. Gas comes off ticket value. |
| Topup | Stripe → 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. |
Honest disclaimers — what the system doesn't do today:
spend_cap tool (default $50/bearer, configurable up to $10K) and blocks create_media when the next call would exceed it. That's a client-side speed bump, not an on-chain limit — a non-MCP path or a stolen key still bottoms out at the wallet balance.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.
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.
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.
get_pricing — bypass_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.
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.
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.
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.
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 = 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.
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.
// 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"
}
price_per_unit from the desired USD (see §2).CAPABILITIES_JSON (Python, not sed).docker compose down && docker compose up -d (not restart)./capabilities and via the MCP get_pricing tool.# 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
simple-infra/tool-host-build/tool-adapter/src/livepeer_tool_adapter/config.py — canonical ToolCapability dataclass.storyboard-a3/docs/byoc-payment-fleet-2026-05-conclusion.md — full retrospective of the pricing + fleet rollout.storyboard-a3/docs/pricing-v1/ — original pricing-table design notes and per-cap rates.