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.
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:
- First request without
X-Payment→ server returns402 Payment Requiredwith x402 challenge headers (X-Payment-Required/PAYMENT-REQUIREDbase64, plusWWW-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/acceptsmirror the header's challenge for clients that only read the body - Sign the EIP-3009 transfer authorization for $0.011 USDC on Base (
$0.01enrolment +$0.001transaction fee — the requirements say11000micro-USDC) - Retry the same request with
X-Payment: <base64>(Payment-Signatureis accepted too) → server verifies, rejects a reused authorization (402,code: "PAYMENT_REPLAY"), registers the portrait, settles the payment after registration succeeds, returns theta_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
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?
Pass the ta_xxx you just enrolled as real_face_asset_id on a Seedance call.
Same ta_xxx mechanic but for a real person — requires liveness, still no government ID.
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.