You have a working LiveKit agent. You want to put it on the phone, or on ClutchCall’s QUIC transport. You do not want to rewrite it, and you do not want to operate a LiveKit server. This recipe builds that end to end:
  1. Install the transport plugin into your existing agent process.
  2. Run the process as a worker that dials out to the edge.
  3. Point a ClutchCall agent at the worker.
  4. Route an inbound number.
  5. Place a real call.
Your agent code does not change. Your AgentSession, your STT/LLM/TTS plugins, your provider keys, and your tool calls all stay verbatim. Only the bytes on the wire are ours.
This is the plugin path. If you must keep your LiveKit server running (other participants join the room), use the front-proxy sidecar instead: Bridge a LiveKit Agent. The integration reference compares both paths.

What you’ll build

The gateway owns the hard edge: a SIP trunk or a browser leg over QUIC on the single :443 plane. It decodes that edge to the runtime’s 8 kHz PCM bus. Your worker holds one outbound QUIC/WebTransport session to the edge. The session carries three lanes:
  • presence (liveness),
  • audio over datagrams, and
  • per-call control (job assign, flush, barge-in, playback finished) over a stream.
Why the agent is the client. Your worker dials out and opens no inbound ports. It works behind NAT, in a private subnet, or in a container with no public address. Nothing about your network has to change.

Prerequisites

required
A working livekit-agents (Python) or @livekit/agents (TypeScript) agent with an entrypoint, plus the provider keys it already uses.
required
A media app key + secret for your tenant, in CLUTCHCALL_MEDIA_KEY and CLUTCHCALL_MEDIA_SECRET. These are distinct from your API credentials. Issue them from the console (agent.clutchcall.dev) or the control-plane API.
required
required
The worker needs QUIC egress to engine.clutchcall.dev. The WebSocket fallback covers presence only. Media over TCP is a degraded last resort.

Build the mini-app

1

Start from your unmodified agent

Any LiveKit agent works. It needs no ClutchCall awareness. This is the file that you already have, untouched:
my_agent.py
Your entrypoint still calls session.start(room=ctx.room, ...). Leave it. The plugin wraps AgentSession.start, binds our audio IO, and drops the room argument. The SDK skips its own RoomIO whenever custom audio IO is set, so the code never touches the room.
2

Install the transport plugin

The quic extra pulls in aioquic. Without it, the package imports but cannot connect.
3

Run it as a worker

Replace your worker runner (cli.run_app(...)) with ours. This is the only new file in your repo.
worker.py
The worker authenticates, registers under agent_name, and waits. You should see it report ready and await calls. It stays connected with a keepalive. If the link drops, it reconnects with exponential backoff. An engine restart therefore does not strand it on a dead socket.
sample_rate must match the call leg. The pcm16 codec is a raw passthrough. A wideband TTS voice (24 kHz / 48 kHz) sent onto an 8 kHz telephony leg plays at the wrong speed. The plugin declares the wire rate on the session output, and the SDK resamples to it. But the SDK resamples only to the rate that you set here. Use 8000 for PSTN.
4

Point a ClutchCall agent at the worker

The engine models a remote agent as a VENDOR_BRIDGE node whose vendor_provider is clutchcall. The decision lives in the agent config, so your dialplan and trunk routing stay untouched.
agent-config.json
With this config, the runtime runs no turn detection and no model in the engine. It hands the caller’s audio to your worker. It streams your agent’s audio back onto the call. The full field reference is in Runtime Configuration.
5

Route a number to the agent

Attach an inbound DID or SIP trunk to this agent. Use the same routing that you would use for any native ClutchCall agent. See SIP Trunking and Number Provisioning.
6

Connect a caller

Dial the number. To drive it programmatically, place an outbound call from the SDK. On answer, the engine dispatches to your worker in the same way as for an inbound call:

What your agent sees

Your entrypoint gets a JobContext whose ctx.room is a veneer, not a LiveKit room. It carries the fields that agents actually read. The fields mirror how LiveKit surfaces SIP participant attributes. An agent that reads the caller’s number therefore keeps working unchanged: Room APIs beyond these fields are not backed. This includes track publication, participant enumeration, and room data messages. There is no room behind the veneer.

Verify it worked

The gateway gives this agent everything that it gives a native agent. The caller edge is identical:
  • Call events & CDR — the call appears in reports with an initiated → established → cleared lifecycle and a Q.850 clear cause.
  • Media quality — per-call MOS / jitter / loss scoring lands on the CDR.
  • Recording — the engine captures both legs if recording is enabled for the trunk.
  • Transcripts — the plugin forwards finalized turns over the control stream. Your remote agent’s history lands in the same store as a native agent’s history.
See Observability for where these appear.

Troubleshooting

Presence is tied to a live connection. If the worker’s session drops and the reconnect did not land yet, dispatch finds nothing. Check that the worker logs a ready line after the most recent reconnect. Check that vendor_room in the agent config exactly matches agent_name in the worker config.
The cause is almost always a sample-rate mismatch. Check sample_rate / sampleRate against the leg (8000 for PSTN). The plugin paces and re-chunks outbound audio into steady 20 ms frames, so it handles bursty TTS. It does not handle a wrong rate.
Media travels on QUIC datagrams. Check that outbound UDP/443 to engine.clutchcall.dev is open. A network that permits TCP/443 but drops UDP will complete presence and stall media.
The wire codec is raw pcm16 today. Opus is not yet wired. Budget for uncompressed audio between your worker and the edge.

LiveKit integration reference

All three paths side by side: plugin, front-proxy, browser shim.

Bridge a LiveKit Agent

The front-proxy sidecar. Use it when your LiveKit room must keep running.

From Self-Hosted LiveKit

The full migration plan: trunks, tokens, rollout.

Runtime Configuration

Every pipeline node field, including VENDOR_BRIDGE.