Avatar service
A service endpoint that returns a branded avatar for any user: photoreal, illustrated, or pixel, deterministic from a seed (user ID / email hash / initials). Built around a fallback hierarchy — real photo → upload → AI from initials → static default — with caching by seed so you pay once per user.
When to Use
- New user signup where you need an immediate profile image.
- Comment systems or forums that default to gravatars.
- Team pages, directories, leaderboards without uploaded photos.
- Placeholder avatars during onboarding before the user picks one.
- Branded alternative to Gravatar / UI Avatars / DiceBear.
- Mock data for staging/demo environments.
Not for: deepfaking real people from a single photo (wrong tool + wrong ethics), or stylizing an already-great uploaded photo — let users keep their own.
Prerequisites
Before building, ask (one message, 5 questions):
- Seed source: user ID, email hash, username, initials? Is it stable forever?
- Style: photoreal portrait, illustrated (Pixar/Disney-ish), flat vector, pixel art, claymorphic, monogram?
- Brand palette: accent color for backgrounds + monogram fallbacks.
- Storage + cache: S3/CDN + DB column, Picsart Drive URL, or KV key? TTL?
- Fallback chain: photo → AI avatar → initials-on-color → default SVG? Order matters.
Skip if the user hands over a detailed brief.
How to Run
Decide the fallback hierarchy first. This is the whole service:
1. user.photo_url → return as-is
2. cache.get(seed) → return stored AI avatar URL
3. gen-ai generate → store URL + return
4. initials on brand color → SVG fallback (no credits)
5. static /default.png → last resort
Estimate + validate model.
gen-ai models info recraftv4
gen-ai pricing recraftv4
gen-ai generate -m recraftv4 -p "illustrated avatar, seed abc123" \
--aspect-ratio 1:1 --dry-run --debug
Deterministic seed → prompt. Hash the seed into stable attribute picks. Never inject Math.random() or timestamps.
SEED="u_482c91"
# Pick palette + mood from the seed, not from RNG
HUE=$(printf "%s" "$SEED" | shasum | cut -c1-2) # 0-255 from hash
gen-ai generate -m recraftv4 \
-p "illustrated profile avatar, round frame, abstract humanoid silhouette, HSL hue ${HUE}, friendly neutral expression, flat vector, centered composition, studio background" \
--aspect-ratio 1:1 \
--negative-prompt "text, watermark, letters, realistic face, specific person" \
--save-to-drive --drive-folder avatars \
--json --no-input | jq -r '.url'
Cache the URL (not the binary) keyed on the seed. Any store: Redis, KV, Postgres column, S3 + stable filename.
Initials fallback (zero credits). When gen-ai is down, rate-limited, or over budget — return an SVG built from the user's initials on a hue derived from the same seed. Users never see a broken image.
const initials = name.split(' ').map(p => p[0]).slice(0, 2).join('').toUpperCase();
const hue = parseInt(sha1(seed).slice(0, 2), 16);
const svg = `<svg viewBox="0 0 64 64"><rect width="64" height="64" fill="hsl(${hue} 70% 45%)"/><text x="32" y="40" text-anchor="middle" font-size="28" fill="#fff" font-family="system-ui">${initials}</text></svg>`;
Wire the endpoint. /avatar/:seed → check photo → check cache → gen-ai → initials. 302 to CDN URL on success; inline SVG on fallback.
Pre-warm the cache in batch for existing users so your prod rollout doesn't hammer gen-ai live:
gen-ai batch run avatars-backfill.json -c 4 -o ./avatars-out
Quick Reference
Backfill 500 existing users:
{
"defaults": {
"model": "recraftv4",
"aspectRatio": "1:1",
"negativePrompt": "text, watermark, letters, realistic face, specific person"
},
"jobs": [
{ "id": "u_482c91", "prompt": "illustrated profile avatar, round frame, flat vector, hue 42, friendly neutral, studio background" },
{ "id": "u_91ac3b", "prompt": "illustrated profile avatar, round frame, flat vector, hue 128, calm expression, studio background" },
{ "id": "u_7de119", "prompt": "illustrated profile avatar, round frame, flat vector, hue 201, warm smile, studio background" }
]
}
Output lands at ./avatars-out/<user-id>.webp — upload to your CDN under the same key. results.json becomes the seed → URL lookup table.
Quick Reference
| Sub-task |
Model |
Why |
| Illustrated / vector / flat avatars |
recraftv4 |
Clean, consistent, cheap — ideal default |
| Photoreal portrait (synthetic, generic person) |
flux-2-pro |
Best faces; use with generic descriptors only |
| Pixel-art avatars |
recraftv4 with pixel-style prompt |
Lowest credits, stylistic control |
| Stylize an uploaded real photo (opt-in) |
flux-kontext-pro + -i photo.jpg |
Edit model preserves identity |
| Cheap draft / style exploration |
gemini-3.1-flash-image |
Lowest per-call cost |
| Identity-locked variants across sizes |
ideogram-character + -i master.png |
Same face across multiple avatars |
Verify live IDs with gen-ai models --mode image.
Procedure
- Determinism = cacheability. Derive every variable attribute (hue, pose, expression) from a hash of the seed. Same seed → same prompt → same image → one credit forever.
- Version the prompt template. Bump
V=2 in the cache key when you restyle; old avatars age out, new ones regenerate on demand.
- Fallback is not optional. Initials-on-hue SVG must always work. Never let the UI render a broken avatar because gen-ai 429'd.
- Negative prompt matters. Always exclude
text, watermark, letters, realistic face of specific person — avatars must not resemble real celebrities or render accidental copy.
- Don't generate on every page load. Generate once per seed, cache forever (or until template version bump). A viral signup spike must not trigger a gen-ai spike.
- Use Drive for storage.
--save-to-drive --drive-folder avatars gives a stable CDN URL with no extra infra.
- Pre-warm before launch. Batch-generate for existing users; don't migrate live traffic onto a cold cache.
- Respect user uploads. If the user has uploaded a real photo, return that — never override. AI avatars are for missing photos only.
- Rate-limit at the edge. Cap gen-ai calls per IP per hour. A bot probing
/avatar/:random can burn your credit budget in minutes.
Pitfalls
| Pitfall |
Fix |
| Regenerated avatar looks different on every refresh |
Non-deterministic prompt — hash the seed, derive attributes, no RNG/timestamps |
| Cache never invalidates after restyle |
Version the prompt template (V=3 in the cache key) |
| AI renders real-looking faces resembling celebrities |
Add negativePrompt: "realistic face of specific person, celebrity, known individual" |
| Text or watermarks appear in the avatar |
Add negativePrompt: "text, watermark, letters, logos" |
| Bot traffic burns credit budget |
Rate-limit per IP; allow only authed users to hit the generator; fallback to initials for anons |
| Gen-ai 429s cause broken avatar UI |
SVG initials fallback must be the last step in the chain |
| Batch backfill half-fails |
gen-ai batch resume <out> retries only failures |
| Different avatar style across the app |
Commit the prompt template; never let callers pass raw prompts |
Verification
Run gen-ai whoami to confirm authentication, then re-run the failed command with --debug.
Cost & time
| Task |
Credits |
Time |
| 1 avatar (Recraft V4, illustrated) |
1-2 |
~4-8s |
| 1 avatar (Flux 2 Pro, photoreal) |
3-5 |
~10-15s |
| 1 avatar (Gemini 3.1 Flash, draft) |
1 |
~3-5s |
| Initials SVG fallback |
0 |
<1ms |
| Cache read (KV/Redis hit) |
0 |
~2-5ms |
| Batch of 500 at concurrency 4 |
~750-1000 |
~15-20 min |
In steady state, expect >99% cache hit rate — near-zero ongoing cost.
See also
gen-ai-use.md — CLI reference, auth, scripting with --json --no-input / jq
gen-ai-batch.md — manifest shapes, batch resume, rate-limit strategy, Cloudflare Worker pattern
gen-ai-workflows.md — Workflow 6 (headshot studio) for stylizing uploaded photos
dev-og-image-service — same caching + determinism patterns for OG cards
1---2name: dev-avatar-service3description: Deterministic default-avatar generator per user.4license: MIT5---67# Avatar service89A service endpoint that returns a branded avatar for any user: photoreal, illustrated, or pixel, deterministic from a seed (user ID / email hash / initials). Built around a fallback hierarchy — real photo → upload → AI from initials → static default — with caching by seed so you pay once per user.1011## When to Use1213- New user signup where you need an immediate profile image.14- Comment systems or forums that default to gravatars.15- Team pages, directories, leaderboards without uploaded photos.16- Placeholder avatars during onboarding before the user picks one.17- Branded alternative to Gravatar / UI Avatars / DiceBear.18- Mock data for staging/demo environments.1920Not for: deepfaking real people from a single photo (wrong tool + wrong ethics), or stylizing an already-great uploaded photo — let users keep their own.2122## Prerequisites2324Before building, ask (one message, 5 questions):25261. **Seed source**: user ID, email hash, username, initials? Is it stable forever?272. **Style**: photoreal portrait, illustrated (Pixar/Disney-ish), flat vector, pixel art, claymorphic, monogram?283. **Brand palette**: accent color for backgrounds + monogram fallbacks.294. **Storage + cache**: S3/CDN + DB column, Picsart Drive URL, or KV key? TTL?305. **Fallback chain**: photo → AI avatar → initials-on-color → default SVG? Order matters.3132Skip if the user hands over a detailed brief.3334## How to Run35361. **Decide the fallback hierarchy first.** This is the whole service:37 ```38 1. user.photo_url → return as-is39 2. cache.get(seed) → return stored AI avatar URL40 3. gen-ai generate → store URL + return41 4. initials on brand color → SVG fallback (no credits)42 5. static /default.png → last resort43 ```44452. **Estimate + validate model.**46 ```bash47 gen-ai models info recraftv448 gen-ai pricing recraftv449 gen-ai generate -m recraftv4 -p "illustrated avatar, seed abc123" \50 --aspect-ratio 1:1 --dry-run --debug51 ```52533. **Deterministic seed → prompt.** Hash the seed into stable attribute picks. Never inject `Math.random()` or timestamps.54 ```bash55 SEED="u_482c91"56 # Pick palette + mood from the seed, not from RNG57 HUE=$(printf "%s" "$SEED" | shasum | cut -c1-2) # 0-255 from hash58 gen-ai generate -m recraftv4 \59 -p "illustrated profile avatar, round frame, abstract humanoid silhouette, HSL hue ${HUE}, friendly neutral expression, flat vector, centered composition, studio background" \60 --aspect-ratio 1:1 \61 --negative-prompt "text, watermark, letters, realistic face, specific person" \62 --save-to-drive --drive-folder avatars \63 --json --no-input | jq -r '.url'64 ```65664. **Cache the URL** (not the binary) keyed on the seed. Any store: Redis, KV, Postgres column, S3 + stable filename.67685. **Initials fallback (zero credits).** When gen-ai is down, rate-limited, or over budget — return an SVG built from the user's initials on a hue derived from the same seed. Users never see a broken image.69 ```ts70 const initials = name.split(' ').map(p => p[0]).slice(0, 2).join('').toUpperCase();71 const hue = parseInt(sha1(seed).slice(0, 2), 16);72 const svg = `<svg viewBox="0 0 64 64"><rect width="64" height="64" fill="hsl(${hue} 70% 45%)"/><text x="32" y="40" text-anchor="middle" font-size="28" fill="#fff" font-family="system-ui">${initials}</text></svg>`;73 ```74756. **Wire the endpoint.** `/avatar/:seed` → check photo → check cache → gen-ai → initials. 302 to CDN URL on success; inline SVG on fallback.76777. **Pre-warm the cache in batch** for existing users so your prod rollout doesn't hammer gen-ai live:78 ```bash79 gen-ai batch run avatars-backfill.json -c 4 -o ./avatars-out80 ```8182## Quick Reference8384Backfill 500 existing users:8586```json87{88 "defaults": {89 "model": "recraftv4",90 "aspectRatio": "1:1",91 "negativePrompt": "text, watermark, letters, realistic face, specific person"92 },93 "jobs": [94 { "id": "u_482c91", "prompt": "illustrated profile avatar, round frame, flat vector, hue 42, friendly neutral, studio background" },95 { "id": "u_91ac3b", "prompt": "illustrated profile avatar, round frame, flat vector, hue 128, calm expression, studio background" },96 { "id": "u_7de119", "prompt": "illustrated profile avatar, round frame, flat vector, hue 201, warm smile, studio background" }97 ]98}99```100101Output lands at `./avatars-out/<user-id>.webp` — upload to your CDN under the same key. `results.json` becomes the seed → URL lookup table.102103## Quick Reference104105| Sub-task | Model | Why |106|----------|-------|-----|107| Illustrated / vector / flat avatars | `recraftv4` | Clean, consistent, cheap — ideal default |108| Photoreal portrait (synthetic, generic person) | `flux-2-pro` | Best faces; use with generic descriptors only |109| Pixel-art avatars | `recraftv4` with pixel-style prompt | Lowest credits, stylistic control |110| Stylize an uploaded real photo (opt-in) | `flux-kontext-pro` + `-i photo.jpg` | Edit model preserves identity |111| Cheap draft / style exploration | `gemini-3.1-flash-image` | Lowest per-call cost |112| Identity-locked variants across sizes | `ideogram-character` + `-i master.png` | Same face across multiple avatars |113114Verify live IDs with `gen-ai models --mode image`.115116## Procedure117118- **Determinism = cacheability.** Derive every variable attribute (hue, pose, expression) from a hash of the seed. Same seed → same prompt → same image → one credit forever.119- **Version the prompt template.** Bump `V=2` in the cache key when you restyle; old avatars age out, new ones regenerate on demand.120- **Fallback is not optional.** Initials-on-hue SVG must always work. Never let the UI render a broken avatar because gen-ai 429'd.121- **Negative prompt matters.** Always exclude `text, watermark, letters, realistic face of specific person` — avatars must not resemble real celebrities or render accidental copy.122- **Don't generate on every page load.** Generate once per seed, cache forever (or until template version bump). A viral signup spike must not trigger a gen-ai spike.123- **Use Drive for storage.** `--save-to-drive --drive-folder avatars` gives a stable CDN URL with no extra infra.124- **Pre-warm before launch.** Batch-generate for existing users; don't migrate live traffic onto a cold cache.125- **Respect user uploads.** If the user has uploaded a real photo, return that — never override. AI avatars are for *missing* photos only.126- **Rate-limit at the edge.** Cap gen-ai calls per IP per hour. A bot probing `/avatar/:random` can burn your credit budget in minutes.127128## Pitfalls129130| Pitfall | Fix |131|---------|-----|132| Regenerated avatar looks different on every refresh | Non-deterministic prompt — hash the seed, derive attributes, no RNG/timestamps |133| Cache never invalidates after restyle | Version the prompt template (`V=3` in the cache key) |134| AI renders real-looking faces resembling celebrities | Add `negativePrompt: "realistic face of specific person, celebrity, known individual"` |135| Text or watermarks appear in the avatar | Add `negativePrompt: "text, watermark, letters, logos"` |136| Bot traffic burns credit budget | Rate-limit per IP; allow only authed users to hit the generator; fallback to initials for anons |137| Gen-ai 429s cause broken avatar UI | SVG initials fallback must be the last step in the chain |138| Batch backfill half-fails | `gen-ai batch resume <out>` retries only failures |139| Different avatar style across the app | Commit the prompt template; never let callers pass raw prompts |140141## Verification142143Run `gen-ai whoami` to confirm authentication, then re-run the failed command with `--debug`.144145## Cost & time146147| Task | Credits | Time |148|------|---------|------|149| 1 avatar (Recraft V4, illustrated) | 1-2 | ~4-8s |150| 1 avatar (Flux 2 Pro, photoreal) | 3-5 | ~10-15s |151| 1 avatar (Gemini 3.1 Flash, draft) | 1 | ~3-5s |152| Initials SVG fallback | 0 | <1ms |153| Cache read (KV/Redis hit) | 0 | ~2-5ms |154| Batch of 500 at concurrency 4 | ~750-1000 | ~15-20 min |155156In steady state, expect >99% cache hit rate — near-zero ongoing cost.157158## See also159160- `gen-ai-use.md` — CLI reference, auth, scripting with `--json --no-input` / `jq`161- `gen-ai-batch.md` — manifest shapes, batch resume, rate-limit strategy, Cloudflare Worker pattern162- `gen-ai-workflows.md` — Workflow 6 (headshot studio) for stylizing uploaded photos163- `dev-og-image-service` — same caching + determinism patterns for OG cards