MOHOLO · embed · private beta

Put a live avatar on your site

One iframe or one script tag. The avatar listens on the visitor's microphone, answers out loud through a live language model, and renders as a real-time 3D head in the browser: no video stream, no plugin, no account. Free during the beta with the key free; a key of your own stores your prompt, avatar and voice and removes the limits.

The easiest way: one iframe

If you can paste HTML into your page, you can add the avatar. No script, no account, no code: one line, and the prompt is part of it. Create yours in the panel (email, pick the avatar, write the prompt, copy the line) or write it by hand with the free key:

<iframe src="https://moholo.ai/embed?key=free&avatar=ana&prompt=You%20are%20Ana%2C%20the%20concierge%20of%20Casa%20Solana..."
        allow="microphone; camera; autoplay; fullscreen" allowfullscreen
        style="border:0;width:100%;height:560px;max-width:100%;border-radius:12px"></iframe>

The prompt goes in the address, so use the setup page to encode it (spaces, commas and quotes become %20 and friends). With a paid key the prompt, avatar and voice are stored with the key, and the line shrinks to src="https://moholo.ai/embed?key=YOURKEY"; you change the prompt by writing to us, not by touching the page.

WordPress

  1. Edit the page or post, add a block, choose Custom HTML.
  2. Paste the iframe line. Click Preview: the avatar appears in the editor.
  3. Publish. The page must be served over https for the microphone (every WordPress host does this).

Self-hosted WordPress and WordPress.com Business/Commerce plans accept iframes in Custom HTML. The free and Personal WordPress.com plans strip iframes and scripts; those need the Business plan or a host that allows embeds. Wix, Squarespace, Webflow, Shopify and Framer: use their "Embed" or "Custom code" element and paste the same line. The <script> tag below does the same thing with a little less typing where scripts are allowed.

Your own model, unlimited minutes: the same iframe

In the panel, choose "my own model key" and paste your Gemini API key: it is stored on our servers only, the conversation runs on your Google account, and the page still carries only the iframe. Nothing to host. (If you prefer to keep the model key on your own server, the bring-your-own-model API below is the alternative.)

Paste this where the avatar should appear (script version)

Parameters

attributemeaningdefault
data-qualitydetail level: low (25k gaussians), medium (50k), high (100k) or full. Omit it and the avatar picks by device: phones get low, a small frame medium, a normal desktop frame high, a large one full.optional
data-keyyour embed key. free works for everyone during the beta.required
data-promptthe system prompt for the live model: who the avatar is, what it knows, how it should answer. Up to 8000 characters (about three pages).MOHOLO's own persona
data-avatarwhich avatar to show, by name: ana, karim, selam, valentina, arjan, minjun, mei (this list is read live from https://moholo.ai/api/avatars).the first public avatar
data-width, data-heightsize in pixels or any CSS length. Portrait shapes work best.420 × 560
data-greet0 = the avatar waits for the visitor instead of speaking first.1
data-quiet1 = quiet mode, the same as data-greet="0": nothing is said until the visitor speaks to the avatar, sends a text or starts a video call.0
data-llmclient = bring your own language model and voice; the avatar runs on the device and your page hands it the reply audio (see below).Moholo's hosted model
data-captions1 = show the avatar's reply text under it. Off by default: the iframe shows only the head and the input.off
data-voicethe avatar's voice: Aoede, Kore, Leda, Puck, Charon, Fenrir, Zephyr, Orus.the avatar's own

Quality levels

Every avatar is compacted with MOHOLO's proprietary geometry, appearance and rig compaction and ships in several detail levels: low (25k gaussians, phones), medium (50k), high (100k) and full. Set one per device with data-quality, or leave the attribute out and the avatar picks automatically from the screen: coarse-pointer phones get low, a small frame medium, a normal desktop frame high, a large one full. A level that is not available for an avatar falls back to the next one up, so a request can never fail.

Two ways to run the conversation

The avatar is always rendered and animated on the visitor's device. What differs is who runs the conversation.

Moholo hosted (default)Bring your own live model (data-llm="client")
who talksMoholo's live model and voices, on Moholo's serversyour live model, your voice, on your servers
what reaches Moholothe visitor's microphone audio and text (the conversation)nothing: the page only serves the avatar files. Your page receives the visitor's audio and text and hands back reply audio
lip synccomputed on Moholo's GPUscomputed on the visitor's device (ONNX, works without WebGPU)
costa Moholo key, billed per conversation minute (below)your model's bill (Gemini Live: about $0.005 per minute of speech in and $0.018 per minute out) and nothing to Moholo during the beta

Set up: bring your own live model (your server keeps the key)

Three pieces. Whatever model you use must stream audio: a request-and-reply model followed by a text-to-speech call puts 4 to 6 seconds between the question and the first word, a live model puts 0.5 to 1.5 s. The example below is Google's Gemini Live; OpenAI's Realtime API or any other engine that streams 16-bit PCM works the same way.

1. The tag

<script src="https://moholo.ai/embed.js" data-key="free" data-avatar="ana" data-llm="client"
        data-width="100%" data-height="560" data-bg="https://your.site/lobby-blurred.jpg"></script>
<script src="/concierge.js"></script>

2. Your server: one live session per visitor

A websocket endpoint that opens the model session with your key. Up: the visitor's utterances (binary, int16 mono 16 kHz, already cut at pauses by the avatar) and typed text. Down: the model's audio chunks the moment they exist, plus transcripts and turn boundaries. Python with google-genai and aiohttp, trimmed from the Casa Solana example:

from google import genai; from google.genai import types
from aiohttp import web, WSMsgType
client = genai.Client(api_key=YOUR_GEMINI_KEY)
CFG = types.LiveConnectConfig(response_modalities=['AUDIO'], system_instruction=YOUR_PROMPT,
    speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name='Aoede'))),
    input_audio_transcription=types.AudioTranscriptionConfig(), output_audio_transcription=types.AudioTranscriptionConfig(),
    realtime_input_config=types.RealtimeInputConfig(automatic_activity_detection=types.AutomaticActivityDetection(disabled=True)))

async def live(request):
    ws = web.WebSocketResponse(); await ws.prepare(request)
    async with client.aio.live.connect(model='gemini-3.1-flash-live-preview', config=CFG) as s:
        await ws.send_json({'type': 'ready'})
        async def pump():                       # model -> browser. receive() ends at every turn: re-enter it
            while not ws.closed:
                async for m in s.receive():
                    sc = m.server_content
                    if not sc: continue
                    if sc.interrupted: await ws.send_json({'type': 'interrupted'})
                    if sc.output_transcription: await ws.send_json({'type': 'transcript', 'who': 'avatar', 'text': sc.output_transcription.text})
                    if sc.input_transcription: await ws.send_json({'type': 'transcript', 'who': 'user', 'text': sc.input_transcription.text})
                    for p in (sc.model_turn.parts if sc.model_turn else []):
                        if p.inline_data: await ws.send_bytes(p.inline_data.data)      # int16 mono 24 kHz
                    if sc.turn_complete: await ws.send_json({'type': 'turn_complete'})
        task = asyncio.create_task(pump())
        async for msg in ws:                     # browser -> model
            if msg.type == WSMsgType.BINARY:     # one utterance from the avatar's microphone
                await s.send_realtime_input(activity_start=types.ActivityStart())
                for i in range(0, len(msg.data), 32000):
                    await s.send_realtime_input(audio=types.Blob(data=msg.data[i:i+32000], mime_type='audio/pcm;rate=16000'))
                await s.send_realtime_input(activity_end=types.ActivityEnd())
            elif msg.type == WSMsgType.TEXT:     # typed question
                await s.send_client_content(turns=types.Content(role='user', parts=[types.Part(text=json.loads(msg.data)['text'])]), turn_complete=True)
        task.cancel()
    return ws

3. Your page: glue between the avatar and your socket

const ws = new WebSocket('wss://your.site/live'); ws.binaryType = 'arraybuffer'; let said = '';
ws.onmessage = e => {
  if (e.data instanceof ArrayBuffer) { Moholo.speak(new Int16Array(e.data), 24000); return; }   // lips follow within ~15 ms
  const j = JSON.parse(e.data);
  if (j.type === 'transcript' && j.who === 'avatar') Moholo.caption(said += j.text);
  if (j.type === 'turn_complete') { Moholo.end(); said = ''; }
  if (j.type === 'interrupted') Moholo.stop();
};
Moholo.on('utterance', m => ws.send(m.pcm));                                    // the visitor spoke
Moholo.on('text', m => ws.send(JSON.stringify({ type: 'text', text: m.text }))); // the visitor typed

Measured with this setup on the example site: typed question to first word 0.55 s, end of a spoken question to first word about 1.5 s, the avatar adding about 15 ms on top of the audio. Keep the model's key on your server only; the page never sees it. A voice close to the one the avatar was distilled on gives the best lips (Ana: "Aoede"); any voice works. Complete, deployable source: Casa Solana, folder moholo-live/examples/website (Cloud Run, key in Secret Manager).

Reference: the window.Moholo API

call / eventmeaning
Moholo.on('ready', fn)avatar on screen, on-device driver loaded (m.driver.engine = onnx, m.driver.ep = wasm or webgpu)
Moholo.on('utterance', fn)one spoken phrase from the visitor, already segmented by silence: m.pcm (int16, 16 kHz), m.seconds
Moholo.on('text', fn)the visitor typed m.text in the box under the avatar
Moholo.speak(audio, rate, {end})reply audio: Int16Array, Float32Array or an int16 ArrayBuffer, mono, any rate; call once per chunk, end: true on the last (or call Moholo.end())
Moholo.stop()interrupt the avatar
Moholo.caption(text)show the reply text under the avatar
Moholo.mic(true|false)open or close the visitor's microphone from your page (the mic button under the avatar does the same)
Moholo.on('speaking'|'state'|'error', fn)m.on while the avatar talks; m.state = idle, listening, thinking, speaking; m.message

Plans

Everything is set up in the panel: enter your email, open the sign-in link we send, pick the avatar, write the prompt, choose who runs the conversation, copy the line. No password, no card for the trial. The beta form at the bottom of the landing page does the same.

planwhat you getprice
Trialyour own avatar, prompt and voice on our model; every conversation runs one minute, then stops. Also the key free for a quick test without an account (30 minutes and 60 conversations per site per day)$0
Pro10 minutes a month on our model included; plug in your own model key (Gemini today, more soon) for unlimited minutes billed by your provider to you; settings kept; allowed-website lock; no Moholo mark; the animation on our servers or, if you choose, on the visitor's device$10 per month per character
More hosted minuteskeep our model beyond the included 10 minutesper conversation minute, quote from founders@moholo.co
Your own characteran avatar made from your photo or promptone-off, on request

Your model key is stored on our servers only and used to open the conversation for your visitors; it is never sent to the browser or written into your page. The page only ever carries your public embed key (mk_…), which is locked to the websites you list.

How this compares

Every other real-time avatar service renders video on datacentre GPUs and bills every minute the avatar is on screen, with or without your own model. Moholo renders on the visitor's device, which is why unlimited minutes can be a flat fee. Public list prices, September 2026, from the vendors' pricing pages:

serviceper-minute priceplanswhere it renders
Moholo Pro, your own model key$0 (your provider bills you: Gemini Live about $0.01 to $0.02)$10 / month / character, unlimited minutesvisitor's device (lip-sync on our GPU or the device)
Moholo Pro, our model10 minutes a month included, then per minute (quote)$10 / month / charactervisitor's device (lip-sync on our GPU)
HeyGen LiveAvatar≈ $0.10 (lite) to $0.20 (full)$19 / $99 / $475 per month, session caps 5 to 60 min, 5 to 40 concurrentcloud video
Tavus CVI$0.32 to $0.37$59 / month for 100 min, $397 / month for 1,250 min; replicas $40 to $65 eachcloud video
D-ID Agents≈ $0.35 (15 s rounding)from $5.90 / month; streaming API $18 to $198 / monthcloud video
Anam$0.11 to $0.24free 30 mincloud video
Beyond Presence€0.175 to €0.35€49 to €349 / monthcloud video
Lemon Slice≈ $0.16 to $0.21from $8 / monthcloud video
Hedra Live Avatars$0.05usagecloud video
Simli≈ $0.05 pay as you go, ≈ $0.009 at volumeusagecloud (Gaussian splats, server-rendered)
bitHuman≈ $0.01 to $0.04free 99 credits / month; self-host from 1 credit / minon-device (flat photo puppet)
UneeQenterprisefrom ≈ $899 / month plus $20k to $80k integrationcloud video

Example: a hotel page with 2,000 conversations a month of 3 minutes each (6,000 minutes) costs about $600 to $2,200 per month on the cloud-video services above, and $10 on Moholo with your own model plus your model's own bill (Gemini Live: roughly $0.01 to $0.02 per minute, so about $60 to $120).

Requirements

Your page must be served over HTTPS for the microphone to work. The avatar needs WebGL2 (every current desktop and phone browser, including Android WebView apps); WebGPU is used when present but not required. In the hosted mode nothing from your page is sent to us except the parameters above, and the conversation runs between the visitor's browser and MOHOLO. In the bring-your-own-model mode the conversation never touches MOHOLO: only the avatar files are served.

Live example

Questions or a key for your site: founders@moholo.co.