Skip to content

Latest commit

 

History

History
209 lines (161 loc) · 10.3 KB

virtual-portrait.md

File metadata and controls

209 lines (161 loc) · 10.3 KB
title Virtual Portrait Enrollment
description Enroll an AI-generated character (no government ID, no liveness check) as a ta_xxx asset for a consistent likeness across Seedance 2.0 videos. $0.011 USDC.

Virtual Portrait Enrollment

Enroll an AI-generated character image as a Virtual Portrait and get back a ta_xxxxxxxx id you can pass as real_face_asset_id on any Seedance 2.0 / 2.0 Fast / 2.0 Mini call. Use this when you want the same character across multiple videos without a liveness check.

:::note{title="No liveness check"} Use this for AI-generated personas, mascots, avatars, virtual spokespeople — no liveness step needed because the asset is understood to be a synthetic character. For real-person likeness, use RealFace (also no government ID, but requires a brief liveness check on the rights-holder's phone to prove consent). :::

Endpoint POST https://blockrun.ai/api/v1/portrait/enroll
Price $0.011 USDC per enrollment (one-time, settled to BlockRun's Base wallet via x402)
Auth x402 micropayment header — no API key needed
Network Base (USDC, EIP-3009 exact)
Returns ta_xxxxxxxx… asset id for use with real_face_asset_id on Seedance 2.0 / 2.0 Fast / 2.0 Mini

You can use the web UI at blockrun.ai/studio/portrait — wallet connect, paste an image URL, sign once, copy the ta_xxx. The endpoint below is for SDK / programmatic use.

Request

{
  "name": "My Spokesperson",
  "image_url": "https://example.com/character.jpg"
}
Parameter Type Required Description
name string Yes Display name for organization in your portrait list (1–64 chars)
image_url string Yes Public https:// URL pointing to a JPG / PNG / WEBP image. Server-side fetched and registered as a Virtual Portrait asset. Max 10 MB.

What makes a good portrait image

Seedance conditions video generation on the supplied image as a face/character reference. Best results when:

  • Single subject, face clearly visible (front or 3/4 angle, eyes open)
  • Neutral expression — extreme expressions reduce the model's ability to apply prompted emotions
  • Clean background (or at least not visually competing with the subject)
  • Even lighting — heavy shadows on the face degrade character consistency
  • High resolution — 1024×1024 or larger; we downscale gracefully but blurry inputs propagate

Images that fail the upstream content filter (NSFW, recognizable real-celebrity likeness without consent) will be rejected at enrollment.

Payment flow (x402)

Standard BlockRun two-step:

  1. First request without X-Payment → server returns 402 Payment Required with x402 challenge headers (X-Payment-Required / PAYMENT-REQUIRED base64, plus WWW-Authenticate: X402 requirements="…") and a body of { "x402Version": 2, "accepts": [{ "scheme": "exact", "network": "eip155:8453", "amount": "11000", "payTo": "0x…", "maxTimeoutSeconds": 300 }], "error": "Payment Required", "message": "Enrolling a Virtual Portrait costs $0.0110 USDC. …", "price": { "amount": "0.0110", "currency": "USD" }, "paymentInfo": { "network": "base", "asset": "USDC", "x402Version": 2 } } — x402Version/accepts mirror the header's challenge for clients that only read the body
  2. Sign the EIP-3009 transfer authorization for $0.011 USDC on Base ($0.01 enrolment + $0.001 transaction fee — the requirements say 11000 micro-USDC)
  3. Retry the same request with X-Payment: <base64> (Payment-Signature is accepted too) → server verifies, rejects a reused authorization (402, code: "PAYMENT_REPLAY"), registers the portrait, settles the payment after registration succeeds, returns the ta_xxx

Settlement happens after the portrait is successfully enrolled. If enrollment fails (content filter, network error), no payment is taken — the route returns 502 and the caller can retry with a fresh signature. If settlement itself fails after a successful enrollment, BlockRun absorbs the cost rather than leave the user with a paid-but-unrecoverable state.

If you're using clawrouter locally, this flow is fully automatic — just call the endpoint.

Response

{
  "object": "virtual_portrait",
  "asset_id": "ta_abcdef1234567890",
  "group_id": "legacy_rf_…",
  "name": "My Spokesperson",
  "image_url": "https://blockrun.ai/api/media/…",
  "source_image_url": "https://example.com/character.jpg",
  "mirrored": true,
  "created_at": "2026-05-22T14:32:11.000Z",
  "usage": {
    "compatible_models": ["bytedance/seedance-2.0", "bytedance/seedance-2.0-fast", "bytedance/seedance-2.0-mini"],
    "how_to_use": "Pass \"real_face_asset_id\": \"ta_abcdef1234567890\" on a Seedance video generation request."
  },
  "price": { "amount": "0.0110", "currency": "USD" },
  "settlement": {
    "success": true,
    "tx_hash": "0x9f3a…",
    "network": "base"
  }
}

The settlement receipt is also returned in the X-Payment-Response / PAYMENT-RESPONSE headers.

Field Description
asset_id The ta_… id to pass as real_face_asset_id on Seedance
group_id Internal asset-group id, returned for your own reference. No endpoint accepts it — enrollment mints its own group, so there is nothing to pass it back to. Not returned by the portraits listing
image_url The BlockRun-hosted mirror of your image (so the listing thumbnail survives a dead source URL); falls back to the original URL if mirroring failed
source_image_url / mirrored The URL you supplied, and whether the mirror succeeded
usage.compatible_models Which BlockRun video models accept this asset id
settlement.tx_hash The Base settlement transaction (verify on BaseScan)

Examples

::::tabs

:::tab{label="cURL (manual x402)"}

# Step 1: probe — returns 402 with challenge headers
curl -i -X POST https://blockrun.ai/api/v1/portrait/enroll \
  -H "Content-Type: application/json" \
  -d '{"name":"My Spokesperson","image_url":"https://example.com/character.jpg"}'

# Step 2: sign the x402 challenge with your wallet (see x402/payment-flow.md)
# Step 3: retry with the signature
curl -X POST https://blockrun.ai/api/v1/portrait/enroll \
  -H "Content-Type: application/json" \
  -H "X-Payment: $PAYMENT_HEADER" \
  -d '{"name":"My Spokesperson","image_url":"https://example.com/character.jpg"}'

:::

:::tab{label="ClawRouter"} ClawRouter handles the x402 dance for you:

curl -X POST http://localhost:8402/v1/portrait/enroll \
  -H "Content-Type: application/json" \
  -d '{"name":"My Spokesperson","image_url":"https://example.com/character.jpg"}'

:::

::::

Using the enrolled portrait

Take the returned asset_id and pass it as real_face_asset_id on a Seedance call:

curl -X POST http://localhost:8402/v1/videos/generations \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bytedance/seedance-2.0-fast",
    "prompt": "the character smiles warmly and waves at the camera",
    "real_face_asset_id": "ta_abcdef1234567890"
  }'

The same ta_xxx can be reused across as many video generations as you want — you pay the per-clip token cost each time, but you don't re-pay the enrollment fee.

Listing enrolled portraits

GET https://blockrun.ai/api/v1/wallet/<address>/portraits

Returns the list of portraits the given wallet has enrolled. Free (rate-limited to 120 requests / hour / IP, shared with the wallet-reconciliation bucket; responses cacheable for 30 s). Works for both EVM (0x…) and Solana (base58) addresses, though Virtual Portrait enrollment itself is currently Base-only.

{
  "wallet": "0xCC8c44AD3dc2A58D841c3EB26131E49b22665EF8",
  "portraits": [
    {
      "assetId": "ta_abcdef1234567890",
      "name": "My Spokesperson",
      "imageUrl": "https://example.com/character.jpg",
      "createdAt": "2026-05-22T14:32:11.000Z",
      "enrollmentTxHash": "0x9f3a…"
    }
  ],
  "count": 1
}

The video playground at /models/bytedance-seedance-2.0-fast reads this same list and renders it as a dropdown when the connected wallet has any enrolled portraits.

Error codes

Code Body error Cause / fix
400 Invalid request body Missing name, invalid image_url, name > 64 chars
400 image_url must be an http(s) URL URL missing https:// scheme
402 Payment Required First request — sign + retry with X-Payment header
402 Payment verification failed code is PAYMENT_INVALID (signature/amount/recipient mismatch — re-sign), PAYMENT_UNFUNDED (insufficient USDC or expired window) or PAYMENT_BLOCKHASH_STALE; a message explains when known, debug carries the raw reason
402 Payment authorization already used code: "PAYMENT_REPLAY" — each authorization is single-use; sign a fresh one
502 varies Enrollment failed (content filter, image too big, network) — no payment taken, safe to retry with a different image
429 Rate limit exceeded Listing endpoint only (120/hour/IP) — back off per Retry-After header

Storage and privacy

  • Wallet → portrait list is stored in BlockRun's GCS (gs://blockrun-prod-2026-logs/portraits/<wallet>.json)
  • The image URL you supplied is kept for previewing in the UI; the canonical image bytes are stored upstream with the inference provider
  • We do not analyze, redistribute, or use your portrait images for any purpose other than serving the asset back to your wallet and forwarding it for Seedance generation
  • Delete is not currently exposed via API — open a GitHub issue or email vicky@blockrun.ai if you need an entry removed

What's next?

::::cards

:::card{title="Video Generation" href="video-generation.md" icon="Image"} Pass the ta_xxx you just enrolled as real_face_asset_id on a Seedance call. :::

:::card{title="RealFace Enrollment" href="realface.md" icon="Boxes"} Same ta_xxx mechanic but for a real person — requires liveness, still no government ID. :::

:::card{title="x402 Payment Flow" href="../x402/payment-flow.md" icon="Zap"} How the 402 / sign / retry handshake works behind the paid enroll call. :::

::::

Also useful: Portrait Studio UI (web flow for non-developers) · Real-person video walkthrough.