# PZERO > Frontier First AI Marketplace. Top up. Do your thing. Repeat. Text, image, and video inference on a two-sided DIEM marketplace. > Cleared capacity trades between 1¢ and 80¢ on the dollar (minimum 20% off $1 DIEM face). > USDC prepaid AI Credits on Base; cheapest eligible seller offer wins each completion. > Supply is Venice-only (privacy-first upstream). Job correlation via X-Pzero-Request-Id / X-Pzero-Support-Reference — not seller self-attest for the model. ## Hosts Fixed. Nothing to derive. ``` API https://api.pzero.studio web app https://pzero.studio docs https://docs.pzero.studio ``` Every example below uses those literally. ## Integrate (you already have an API key) PZERO is OpenAI-compatible. Any OpenAI SDK or agent harness works with three values: ``` base_url: https://api.pzero.studio/v1 api_key: pzero_YOUR_KEY model: claude-opus-4-8 (or any status:"live" id from GET /v1/models) ``` Two constraints most harnesses should know up front: - Streaming is supported (SSE). Set `"stream": true` for token streaming; omit `stream` or use `"stream": false` for buffered JSON. Retries happen before any SSE bytes are written. - The router may try another eligible supplier if the first cannot fulfill the request — up to five distinct supplier keys before surfacing an error (see docs: Marketplace → Clearing on `https://docs.pzero.studio/marketplace/clearing`). - Buyer concurrency gate is currently disabled in code. When enabled, one in-flight completion per wallet across all keys returns HTTP 429 `buyer_concurrency_limit`. ### curl ``` curl https://api.pzero.studio/v1/chat/completions \ -H "Authorization: Bearer pzero_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "claude-opus-4-8", "messages": [{"role": "user", "content": "Hello"}], "stream": false}' ``` Clients that POST `{base_url}/responses` (Codex) use `POST /v1/responses` on the same host and key: ``` curl https://api.pzero.studio/v1/responses \ -H "Authorization: Bearer pzero_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "claude-opus-4-8", "input": "Hello", "stream": false}' ``` ### OpenAI SDK (Python) ```python from openai import OpenAI client = OpenAI(base_url="https://api.pzero.studio/v1", api_key="pzero_YOUR_KEY") resp = client.chat.completions.create( model="claude-opus-4-8", messages=[{"role": "user", "content": "Hello"}], stream=False, ) ``` ### OpenAI SDK (TypeScript) ```typescript import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'https://api.pzero.studio/v1', apiKey: 'pzero_YOUR_KEY', }); const resp = await client.chat.completions.create({ model: 'claude-opus-4-8', messages: [{ role: 'user', content: 'Hello' }], stream: false, }); ``` Successful responses may include `X-Balance-Remaining` (confirmed USDC, six decimal places). ### What a request cost Every inference response, including errors, carries `X-Pzero-Request-Id` and `X-Pzero-Support-Reference`. Quote the support reference in support requests, or resolve it yourself at `GET /v1/agent/requests/{support_reference}`. Where settlement finishes before the response head — buffered chat, buffered Responses, image, video queue — the response also carries `X-Pzero-Cost-Usdc` and `X-Pzero-Clearing-Price-Cents`. Video queue and quote bodies repeat those values under a `pzero` object. Chat streams cannot use a header, so a streamed chat completion emits one extra SSE frame after the final usage chunk and before `[DONE]`: ```json {"object":"pzero.cost","choices":[],"costUsdc":"0.004212","clearingPriceCents":49,"supportReference":"…"} ``` `choices` is an empty array so the frame matches the shape of the final usage chunk. Clients that only read `choices[0]` when present are unaffected; anything else can ignore the frame and read the cost later from `GET /v1/agent/requests/{support_reference}`. Responses streams do not emit `pzero.cost` or chat `[DONE]`. Cost is flat string metadata (`costUsdc`, `clearingPriceCents`, `supportReference`) on `response.completed` or `response.incomplete`. Settle failure is `response.failed` with no `response.error`. Usage uses Responses names (`input_tokens`, `output_tokens`, `total_tokens`) and is present on completed only. ### Errors Full matrix: docs `https://docs.pzero.studio/agents/errors`. - HTTP 400. Malformed JSON or model not `live`. - HTTP 401. Missing/invalid Bearer, or a session cookie was sent instead of Bearer. - HTTP 402. Confirmed USDC balance insufficient (pending top-ups do not fund inference). - HTTP 404. Model not found; use a `live` model `id` from `GET /v1/models`. - HTTP 429. `key_concurrency_limit` when this API key already has too many completions in flight (retry after ~1s), or `buyer_concurrency_limit` for the per-wallet gate when it is active (currently off). - HTTP 500. Internal error. - HTTP 502. Provider could not complete the request (`upstream_error`; see Clearing in docs). - HTTP 503. No eligible supply: `no_eligible_supply`, `insufficient_routable_at_price`, `pool_empty`, or `key_wait_timeout`. `no_eligible_supply` means supply exists but sits above your ceiling: the body carries `buyerMaxCents`, `cheapestPostedCents`, and `routableDiemAboveMax`, and `PATCH /v1/agent/max-price` with `cheapestPostedCents` clears it. Confirmed credit that will not spend is usually this, not an empty balance. --- ## Get a key and fund it (one-time setup) Inference uses a persistent `pzero_…` API key, not x402 per request and not the session cookie. Setup happens once, then agents only need the Bearer key. ### Fastest for agents: one paid call, no browser `POST /v1/x402/signup` needs no account and no session. The settled payment is the credential: the account is created for the wallet that signed the payment, and the response carries a plaintext key. ``` POST /v1/x402/signup Content-Type: application/json {"amountMicroUsdc": 10000000, "keyName": "my-agent"} ``` The first call returns HTTP 402 with `accepts[]` (same shape as the top-up challenge below). Sign EIP-3009 (`scheme: exact`) and retry with `X-PAYMENT`. HTTP 200 returns `apiKey` (plaintext, shown once), `keyPrefix`, `walletAddress`, `accountId`, `pendingUsdc`, `transaction`, `confirmationBlocks`. There is no `walletAddress` request field. The account wallet is the payer the facilitator verified. The credit it buys is pending until the chain confirms it. The signup credit lands pending, so poll `GET /v1/agent/me` with the new key until `confirmedUsdc` covers the first request — pending balance does not fund inference. If the same settlement is presented again, HTTP 200 comes back with `"idempotent": true` and no `apiKey`. Mint further keys with `POST /v1/agent/keys`, which needs a key you already hold — so store the plaintext from the signup response before doing anything else. There is no way to reissue a lost key from the payment: recovery is either a second signup (new settlement, new key, the amount adds to the same wallet's balance) or a browser session on that wallet at `https://pzero.studio`. A wallet holds at most 10 active keys, counted across every route that mints one. A paid signup on a wallet already at 10 answers HTTP 409 with `credited: true`: the payment is credited and no key is issued. Revoke one with `DELETE /v1/agent/keys/{id}` first. If a payment settles and cannot be turned into balance — no matching on-chain transfer, or a write failed — the answer is HTTP 409 with `transaction` and no `accepts`. Read `reconciling`: `true` means the payment is queued and reconciliation credits whatever the chain confirms, `false` means even that queue write failed and a human has to recover it. Either way the money moved, so do not send the payment again; keep the hash. On a failed signup there is no key to poll with, so the balance is reachable by signing in at `https://pzero.studio` with the paying wallet, or by a fresh signup that credits again and returns a key. HTTP 402 always carries `accepts` and always means the payment has not landed. Top up later with `POST /v1/x402/refill` (Bearer, same body and challenge, payer must be the key's wallet). Inference is never paid per request. ### Fastest for humans: browser 1. Open `https://pzero.studio` and sign in (Privy). 2. Top up USDC (x402; minimum 1 USDC). 3. Create an API key in the account UI (plaintext shown once). ### Headless with a Privy session (web-equivalent setup) Step 1. Session — Privy. ``` POST /auth/privy/verify Content-Type: application/json {"accessToken": "", "identityToken": "", "walletAddress": "0x…", "privyLoginSurface": "wallet"} ``` `walletAddress` must appear in the identity token `linked_accounts`. On success the server sets HttpOnly `pzero_session`. Reuse that cookie jar for the remaining steps. Step 2. Fund the account (x402 top-up). Payer wallet must match the signed-in address: ``` POST /v1/x402/top-up Content-Type: application/json Cookie: pzero_session=… {"amountMicroUsdc": 10000000} ``` Returns HTTP 402 on the first call with payment requirements in the JSON body (`x402Version`, `accepts[]`, `amountMicroUsdc`). Sign EIP-3009 (`scheme: exact`) and retry with the same body plus: ``` X-PAYMENT: ``` HTTP 200 includes `confirmedUsdc`, `pendingUsdc`, `topUpAmountUsdc`, `walletAddress`. Top-ups confirm on-chain after 20 blocks on Base mainnet. Bounds for `amountMicroUsdc` (integer micro-USDC, 6 decimals): minimum `1000000` (= 1 USDC), maximum `1000000000` (= 1000 USDC). Signing a challenge in TypeScript. The library is `x402` (v1.2), the same one this marketplace's own clients use — not the `@x402/*` scoped packages: ```typescript import { createPaymentHeader, selectPaymentRequirements } from 'x402/client'; import { createWalletClient, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { base } from 'viem/chains'; // x402 expects a wallet-client-shaped signer, not a bare LocalAccount. The // transport is never called: EIP-3009 is a signature and the facilitator submits // the transfer. const signer = createWalletClient({ account: privateKeyToAccount(process.env.EVM_PRIVATE_KEY), chain: base, transport: http(process.env.BASE_RPC_URL), }); // Session cookie required for top-up only; POST /v1/x402/signup needs no cookie // and POST /v1/x402/refill uses Bearer. const url = 'https://api.pzero.studio/v1/x402/top-up'; const body = JSON.stringify({ amountMicroUsdc: 10_000_000 }); const headers = { 'Content-Type': 'application/json' }; const challenge = await fetch(url, { method: 'POST', credentials: 'include', headers, body }); const { x402Version, accepts } = await challenge.json(); // HTTP 402 const requirements = selectPaymentRequirements(accepts, 'base', 'exact'); const paid = await fetch(url, { method: 'POST', credentials: 'include', headers: { ...headers, 'X-PAYMENT': await createPaymentHeader(signer, x402Version, requirements) }, body, // byte-identical to the challenged request }); ``` Send the retry with the same body you were challenged on: the amount is part of what was signed. `@pzero/agent-sdk` wraps this loop, including signup, refill and runway top-ups. Step 3. Create the API key (plaintext returned once): ``` POST /v1/account/api-keys Cookie: pzero_session=… Content-Type: application/json {"name": "my-agent"} ``` --- ## 402 Top-Up Response Format x402 version 1 today. `accepts[]` uses `scheme: exact`, `network: base`. ```json { "x402Version": 1, "accepts": [ { "scheme": "exact", "network": "base", "maxAmountRequired": "10000000", "resource": "https://api.pzero.studio/v1/x402/top-up", "description": "Top up 10.000000 USDC of AI Credits", "mimeType": "application/json", "payTo": "0x…", "maxTimeoutSeconds": 120, "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "extra": { "name": "USD Coin", "version": "2" } } ], "amountMicroUsdc": 10000000 } ``` - `maxAmountRequired` and `amountMicroUsdc` are integer micro-USDC (`1000000` = 1.000000 USDC). Not decimal strings. - `payTo` is the deployment settlement address (from server config). - `asset` is USDC on Base mainnet. - `resource` matches `https://api.pzero.studio` (the host that issued the 402). PZERO does not offer `upto` scheme or per-request x402 on `POST /v1/chat/completions`. Fund prepaid credits first, then call inference with Bearer. --- ## Pricing ### Marketplace corridor All cleared DIEM capacity trades between 1¢ and 80¢ per $1 DIEM face value. A new account's `maxPriceCentsPerDollar` defaults to 80¢. You still clear at the cheapest eligible offer, not the ceiling. Pass `maxPriceCentsPerDollar` on signup to start lower, or change it later with `PATCH /v1/agent/max-price`. Platform fee is 10% of gross USDC on each clear. Check `GET /v1/capacity` before your first call: `cheapestPostedCents` is what you must be willing to pay. The router selects the cheapest active offer at or below the buyer's `maxPriceCentsPerDollar`. Buyers do not pick a listing; sequential requests may clear different posted prices. ### Model usage rates Token metering uses Venice catalog rates from `GET /v1/models` (`pricing.input_per_million`, `pricing.output_per_million`, `pricing.cache_per_million`). Values are micro-USDC per million tokens. Actual debit depends on measured usage and the clearing price of the offer that wins. Image rows in `GET /v1/models` use structured flat-job pricing. Video rows use quote-time Venice pricing plus published markup; queue response includes `queue_id` and retrieval streams MP4 when ready. Image requests: PZERO prices `resolution`, `quality`, and `variants`/`n` (1–4) and forwards every other field to Venice unchanged, so Venice defaults apply to what you omit. Generated images are watermarked unless you send `hide_watermark: true`, which costs the same. `enable_web_search` is refused because it bills beyond published pricing. --- ## Image jobs Synchronous. Pick any `modality: "image"`, `status: "live"` row from `GET /v1/models`. ``` POST /v1/image/generate Authorization: Bearer pzero_YOUR_KEY Content-Type: application/json {"model": "chroma", "prompt": "a red balloon over the harbour", "hide_watermark": true} ``` HTTP 200 returns `images`: an array of base64-encoded image data (WebP unless you pass `format`), one entry per variant, plus `id`, the echoed `request`, and `timing`. There is no URL to fetch — decode the base64 yourself. Cost is on `X-Pzero-Cost-Usdc`. ## Video jobs Asynchronous, and priced per job at quote time. Read `constraints` on the model row in `GET /v1/models` first, and send only the fields that row advertises — the keys differ per model. Every video model carries `durations` (e.g. `["5s"]`) and `model_type`; `resolutions`, `aspect_ratios`, and `audio` are present on some models only, so treat a missing key as "do not send that field". `model_type` tells you what input is required: `text-to-video` needs only a prompt, while `image-to-video`, `reference-to-video`, and `video-to-video` need the corresponding media input (`image_url` and related fields — see the OpenAPI request schema). 1. `POST /v1/video/quote` — same body as queue. No charge, no job. Returns `quote_diem` (what the job costs in DIEM), `estimated_usdc` (what you would pay), `max_price_cents` (your ceiling), `max_eligible_offer_diem` (largest single offer at or below that ceiling), `eligible_routable_diem` (total DIEM at or below that ceiling), and `clears_now`. `clears_now` is true when `quote_diem` fits the largest single eligible offer (`max_eligible_offer_diem`). When it is false: - `max_eligible_offer_diem` (or `eligible_routable_diem`) is below `quote_diem` — the market is too thin for a job this size right now. Queue a shorter or cheaper render, or retry later. More money will not help. - `eligible_routable_diem` is `0` while `GET /v1/capacity` reports supply — the supply is posted above your `max_price_cents`. Raise it with `PATCH /v1/agent/max-price` (compare against `cheapestPostedCents`). Separately, compare `estimated_usdc` against `confirmedUsdc` from `GET /v1/agent/me`. Queue can still answer `402` when `clears_now` is true if confirmed balance is too low. Refill with `POST /v1/x402/refill`. Pending credit does not count. Queueing anyway returns `503` (`insufficient_routable_at_price`) or `402`, so quoting first is how you avoid a wasted round trip, not a required step. 2. `POST /v1/video/queue` — settles the charge up front and returns `queue_id`. 3. `POST /v1/video/retrieve` with `{"queue_id": "..."}`, repeatedly, roughly every 5–10s. While rendering you get HTTP 200 with a JSON status body. On delivery you get HTTP 200 with `Content-Type: video/mp4` and the MP4 bytes. Branch on the content type, not the status code. 4. `POST /v1/video/complete` deletes the provider-side media. Optional: the media is already gone once retrieve delivered it, so this can answer 400 after a successful delivery. Stop rules for the poll loop: - Delivered once means delivered: a later retrieve on the same `queue_id` answers `410`. Save the bytes on first delivery. - A JSON body reporting `"status": "COMPLETED"` (rather than MP4 bytes) means the provider finished without serving media. Retrying will not produce a file — stop polling. The job is refunded automatically within 6 hours; a support reference is not needed. - A JSON body reporting `failed`, `error`, or `canceled` is terminal and already refunded. - Renders take minutes, not seconds. Budget for a few hundred polls before treating silence as a fault. --- ## Available Models Call `GET /v1/models` for the public model catalog, or `GET /v1/models/{id}` for one row (same object as that id in the list; bare or `pzero/` id). Use a `status: "live"` entry's `id` (Venice model slug) as the `model` field in chat completions. The catalog may include `status: "coming_soon"` rows when a seen Venice model is not yet enableable (missing published price or token caps). New models become live on the catalog sync when those fields exist. Only `status: "live"` rows are callable and include published pricing. Call `GET /v1/models/stats?window=7d` for relative popularity. Supported windows: `24h`, `7d`, `30d`. Stats expose `rank`, `usageShare`, daily normalized shares, and `hasSufficientVolume`; they do not expose raw request counts, token counts, wallets, API keys, support references, or request ids. Frontier models (Claude Opus/Sonnet, GPT-5x, Gemini Pro tiers) are the promoted product tier. Prefer them for unit economics; small-model volume may be throttled if unprofitable. --- ## Endpoints All paths are relative to `https://api.pzero.studio`. | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | /llms.txt | public | This file | | GET | /openapi.json | public | OpenAPI 3.0 spec | | GET | /healthz | public | Liveness | | GET | /v1/models | public | Public model catalog; live rows include modality pricing | | GET | /v1/models/{id} | public | One public catalog row; same object as that id in GET /v1/models data[] | | GET | /v1/models/stats | public | Relative per-model popularity (rank/share only) | | GET | /v1/market/stats | public | Corridor depth, buyer savings, today's clears | | GET | /v1/capacity | public | Routable DIEM per posted price + live modalities; `?maxPriceCents=30–80` | | GET | /v1/supply/pool/summary | public | Posted-price pool depth (no supplier identity) | | POST | /v1/x402/signup | public | Paid account + key mint (x402 exact); payment is the credential | | POST | /auth/privy/verify | public | Privy access + identity tokens → session cookie | | POST | /auth/logout | session | Revoke session | | GET | /v1/me | session | `accountId`, `walletAddress`, `authProvider` | | GET | /v1/account | session | Balances, max price, recent ledger | | PATCH | /v1/account/max-price | session | Set `maxPriceCentsPerDollar` (30–80) | | POST | /v1/account/api-keys | session | Create `pzero_…` key (plaintext once) | | DELETE | /v1/account/api-keys/{id} | session | Revoke key | | GET | /v1/account/savings | session | Savings vs face and ceiling | | GET | /v1/account/inference-history | session | Buyer inference history | | POST | /v1/x402/top-up | session | Fund prepaid USDC (x402 exact) | | POST | /v1/x402/refill | Bearer | Fund prepaid USDC without a browser; payer must be the key's wallet | | GET | /v1/agent/me | Bearer | Wallet, balances, max price, confirmation blocks | | GET | /v1/agent/requests/{support_reference} | Bearer | One own request: cost, clearing price, status, timings | | POST | /v1/agent/keys | Bearer | Mint another key on your own account (max 10 active) | | GET | /v1/agent/keys | Bearer | List active keys (metadata only) | | DELETE | /v1/agent/keys/{id} | Bearer | Revoke one own key, including the calling key | | PATCH | /v1/agent/max-price | Bearer | Set your own `maxPriceCentsPerDollar` (30–80); what `no_eligible_supply` asks you to raise | | POST | /v1/chat/completions | Bearer | OpenAI-compatible chat (buffered or SSE streaming) | | POST | /v1/responses | Bearer | Stateless Responses facade (Codex); same money path as chat | | POST | /v1/image/generate | Bearer | Venice image generation | | POST | /v1/images/generations | Bearer | OpenAI-compatible image generation | | POST | /v1/video/queue | Bearer | Quote, settle, and queue async video | | POST | /v1/video/quote | Bearer | Preview video cost (no settle, no job) | | POST | /v1/video/retrieve | Bearer | Poll/stream video result | | POST | /v1/video/complete | Bearer | Delete Venice-hosted video media | | POST | /v1/supply/venice-keys | session | Register supplier Venice API key | | GET | /v1/supply/venice-keys/mine | session | List your registered keys | | DELETE | /v1/supply/venice-keys/{id} | session | Revoke a registered key | | POST | /v1/supply/offers | session | Post DIEM supply offer | | GET | /v1/supply/offers/mine | session | List your offers (excludes cancelled; includes keyStatus) | | PATCH | /v1/supply/offers/{id} | session | Reprice, pause, or unpause an offer | | DELETE | /v1/supply/offers/{id} | session | Cancel a live (active/paused) offer | | GET | /v1/supply/ledger | session | Supplier ledger | | GET | /v1/supply/inference-history | session | Supplier inference history | --- ## Payment Methods ### 1. Prepaid USDC + Bearer API key (primary agent path) Agent, no browser: `POST /v1/x402/signup` → `Authorization: Bearer pzero_…` → `POST /v1/x402/refill` before the balance runs out. Human: sign in (Privy) → x402 top-up → create `pzero_…` key. ### 2. x402. Account funding only Exact scheme on Base. Header `X-PAYMENT` (base64 JSON payload) on `/v1/x402/signup` (no auth; payer becomes the account), `/v1/x402/refill` (Bearer; payer must be the key's wallet) and `/v1/x402/top-up` (session; payer must match the signed-in wallet). Never used for per-request inference settlement. --- ## Contracts (Base, chain ID 8453) - USDC: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` - Top-up `payTo`: returned per deployment in 402 `accepts[].payTo` - Facilitator: Coinbase CDP (`https://api.cdp.coinbase.com/platform/v2/x402`) on mainnet --- ## For Suppliers Monetize expiring Venice DIEM inventory. Session required (Privy); no separate seller API key type. 1. Sign in: `POST /auth/privy/verify` → session cookie 2. `POST /v1/supply/venice-keys` with `{"veniceApiKey": "…", "label": "…"}` 3. `POST /v1/supply/offers` with `{"priceCentsPerDollar": 50, "quantityDiem": "10.000000", "veniceKeyId": "…"}` Rules: - `priceCentsPerDollar` must be 1–80 (cents per $1 DIEM face). - One live offer (`active` or `paused`) per registered Venice key. Pause occupies the slot; `DELETE /v1/supply/offers/{id}` or key revoke frees it. - Platform keeps 10% of gross USDC per clear; remainder credits the supplier wallet. - DIEM expires at UTC midnight; unsold inventory may expire below floor rather than clear sub-1¢. Supplier Venice API keys are encrypted at rest (AES-256-GCM). `DELETE /v1/supply/venice-keys/{id}` revokes the platform key row and cancels its non-cancelled offers (`active`, `paused`, `depleted`, `expired`); already-revoked keys still heal leftover offers. `DELETE /v1/supply/offers/{id}` cancels a live (`active`/`paused`) offer. Pause (`PATCH` status `paused`) only stops routing; it is not cancel. Revoke the credential at Venice for upstream invalidation. --- ## Discovery - `https://api.pzero.studio/llms.txt` — this file (canonical) - `https://api.pzero.studio/SKILL.md` — the same integration as an installable agent skill (YAML frontmatter, points back here for detail) - `https://api.pzero.studio/MCP.md` — hosted MCP connector setup (Cursor, Claude, Claude Code, any remote MCP client) - `https://api.pzero.studio/openapi.json` — machine-readable OpenAPI 3.0 spec - `https://api.pzero.studio/pricing.md` — how a price is arrived at, and which endpoint carries the live number - `https://api.pzero.studio/marketplace.md` — why supply is priced the way it is, and what makes a request unroutable - `https://api.pzero.studio/v1/capacity` — how much inventory is buyable right now, and at what posted prices - `https://pzero.studio/llms.txt` — same file, proxied by the web app - `https://pzero.studio/SKILL.md` — same skill file, proxied by the web app - `https://pzero.studio/MCP.md` — same MCP guide, proxied by the web app - `https://pzero.studio/agents/quickstart` — human-readable Agents quick start in the web app - `https://pzero.studio/marketplace` — public corridor depth board (supply vs demand) - x402 Bazaar — `POST /v1/x402/signup` (no account needed) and `POST /v1/x402/top-up` are the discoverable x402 resources (exact scheme, CDP facilitator) The web app also proxies the API under `https://pzero.studio/api/…`; prefer the dedicated `https://api.pzero.studio` host for integrators.