How to Build a Sales Coaching Tool That Analyzes Live Calls
The architecture of live sales coaching: calendar auto-join, live transcripts with speaker names, metrics computed from end_of_turn events, private nudges in your own app, CRM write-back, consent and cost per coached hour.
A live sales coaching tool has four parts: a bot that joins every sales call from the rep's calendar, a live transcript with speaker names, a scoring loop that runs each time someone finishes a turn, and a private screen where the rep sees nudges. You build the scoring and the rep's view. A meeting bot API handles the call.
MeetStream is a meeting bot API for AI agents: one API call sends a bot into Zoom, Google Meet or Microsoft Teams that can hear the room, speak and act, and stream audio and transcripts live. This guide covers the coaching architecture, the metrics you can compute live and how, CRM write-back, consent, and cost per coached hour.
Updated October 2026. Written by Maddy, co-founder and CPO of MeetStream, which has run about 80,000 meeting bots and processed over a million meeting minutes. The product calls here come from one question: what will a rep tolerate in the middle of a live deal?
What is the architecture of a live sales coaching tool?
It is a loop with one input and two outputs. The input is the event stream from a bot in the call. The outputs are private nudges to the rep during the call, and a scorecard plus a CRM update after it.
- Join. A bot joins each external meeting on the rep's calendar as a named participant.
- Listen. Live transcript events arrive at your webhook with a speaker name and word timings on every event.
- Score. On each
end_of_turn, code updates the cheap metrics; a language model (LLM) handles the few judgments code cannot make. - Nudge. Your server pushes nudges to the rep's side panel through your own channel, not the meeting.
- Write back. When the bot leaves, you write a summary, next steps and metrics to the CRM, and run deeper analysis on per-participant audio.
How does the bot get into every sales call?
Connect each rep's calendar and schedule a bot for every external meeting. Google Calendar and Microsoft Calendar integration is free on MeetStream, and the bot joins as a named participant, so reps never paste links.
Filter before you schedule. Coaching an internal one-to-one is a fast way to lose a rep's trust, so only schedule meetings with at least one attendee outside your company's email domain. For a known time you can also pass join_at on create_bot, and DELETE /api/v1/calendar/scheduled_bots/{bot_id} cancels a scheduled bot when a rep opts a call out. Put the rep and deal IDs in custom_attributes: they come back on every webhook and every live transcript event, so the scoring loop never has to look them up.
Sales calls are often hosted by the buyer, which changes who admits the bot. On Google Meet and Teams a guest bot may wait in the buyer's lobby, so on bot.in_waiting_room tell the rep, in their panel, to admit their notetaker. Set automatic_leave.waiting_room_timeout and expect bot.notallowed if nobody does.
On Zoom, bots need a one-time Marketplace app setup, and meetings outside your app's account need an OBF token, which can only be minted for a user who authorized your app and is in the meeting. Zoom's OBF FAQ says the session ends when that user leaves. For coaching that fits: the rep is the authorizing user, so the bot is there exactly when the rep is. Recording on Zoom also waits for host consent, so handle bot.recording_permission_denied by telling the rep the host declined recording.
Why should coaching nudges go to the rep's own app?
Because everything a bot does inside the meeting is public. Bot audio is heard by the whole meeting, there is no documented way to play audio privately to one participant, and messages sent with POST /api/v1/bots/{bot_id}/send_message land in the meeting chat, which the buyer can read.
A chat line saying "you have talked for four minutes" is coaching for the rep and an awkward message for the buyer. So deliver nudges where only the rep looks: a browser extension side panel next to the meeting tab, a desktop overlay, or a web app on a second screen. Your server pushes to it over a WebSocket or server-sent events, keyed by the rep ID you set in custom_attributes.
Keep the bot's visible presence for disclosure: its name, the bot_message it posts when it joins, and an image card set with send_image. More on that in the consent section below.
Which coaching metrics can you compute live, and from what?
Most coaching metrics are arithmetic on finished turns: who spoke, from when to when, and what they said. Compute those in code on every end_of_turn, and save the LLM for judgments like next steps.
| Metric | How to compute it from the event stream | When |
|---|---|---|
| Talk ratio | Speech seconds per speaker from the union of word start and end times in finished turns, divided by the total | Live |
| Longest monologue | Consecutive finished turns by one speaker, merged while the gap stays under about 1.5 s; keep the maximum | Live |
| Questions asked | Rep turns whose final punctuated_word ends in "?", plus an LLM check for indirect questions | Live |
| Filler words | Count "um", "uh" and "you know" in the rep's final words; skip "like" and "so" in a first version | Live |
| Speaking pace | Final words divided by speech minutes, per speaker | Live |
| Interruptions | Live proxy: a new speaker's words before the current speaker's end_of_turn. Exact overlaps: per-participant audio after the call | Proxy live, exact post-call |
| Next steps | LLM over the last few finished turns: owner, action, date | Near the end, final post-call |
Commit on end_of_turn because interim words change: each word carries word_is_final, and a word marked false may come back different. Question marks depend on your provider formatting punctuation, so keep the LLM fallback. Set thresholds from your own won and lost calls rather than a number from someone else's study; a talk ratio that suits a discovery call is wrong for a demo.
Interruptions are the metric people get wrong. MeetStream's live audio is mixed, with one dominant speaker tagged per frame, so two people talking at once look like one. Treat the live number as a proxy. After the call, per-participant audio files from GET /api/v1/bots/{bot_id}/get_audio_streams show real overlaps. They are off by default (set audio_separate_streams to true) and included in the base rate. On Zoom each person is fully isolated; on Google Meet and Teams separation is partial, so treat overlap counts there as estimates. See per-participant audio and diarization vs per-participant audio.
How do you compute talk time per speaker from live transcript events?
Collect each speaker's words until end_of_turn, then add the union of their time spans to that speaker's total. The union matters: interim words are re-sent as they firm up, so summing every word you receive counts the same speech twice.
The Flask service below creates a coaching bot with a streaming provider and a live transcript webhook, commits turns, and tracks talk ratio and the rep's longest monologue. The payload fields are the ones in the live transcription guide.
import os, requests
from collections import defaultdict
from flask import Flask, request
API = "https://api.meetstream.ai/api/v1"
PAUSE, MONOLOGUE_GAP = 0.5, 1.5 # seconds: pauses inside speech, gaps that end a monologue
def create_coaching_bot(meeting_link, rep_name, deal_id, base_url):
r = requests.post(f"{API}/bots/create_bot",
headers={"Authorization": f"Token {os.environ['MEETSTREAM_API_KEY']}"},
json={"meeting_link": meeting_link, "bot_name": f"{rep_name}'s notetaker (recording)",
"callback_url": f"{base_url}/lifecycle",
"live_transcription_required": {"webhook_url": f"{base_url}/live-transcript"},
"recording_config": {"transcript": {"provider": {"deepgram_streaming": {"transcription_mode": "sentence"}}}},
"audio_separate_streams": True, # per-person files for post-call overlap analysis
"custom_attributes": {"rep_name": rep_name, "deal_id": deal_id}})
r.raise_for_status()
return r.json()["bot_id"]
def speech_seconds(spans):
"""Union of word spans, so re-sent interim words are never counted twice."""
total, start, end = 0.0, None, None
for s, e in sorted(spans):
if end is not None and s <= end + PAUSE:
end = max(end, e)
continue
if end is not None:
total += end - start
start, end = s, e
return total + (end - start if end is not None else 0.0)
app = Flask(__name__)
calls = defaultdict(lambda: {"open": defaultdict(dict), "talk": defaultdict(float),
"run": None, "longest": defaultdict(float)})
@app.post("/live-transcript")
def live_transcript():
ev = request.get_json(force=True)
call, speaker = calls[ev["bot_id"]], ev.get("speakerName") or "NoSpeaker"
for w in ev.get("words", []): # interim words come back as they firm up: key by start time
call["open"][speaker][round(w["start"], 2)] = (w["start"], w["end"])
if ev.get("end_of_turn"):
spans = list(call["open"].pop(speaker, {}).values())
if spans:
commit_turn(call, speaker, spans)
push_to_rep(ev["bot_id"], metrics(call, ev["custom_attributes"]["rep_name"]))
return "", 204 # acknowledge fast; do slow work elsewhere
def commit_turn(call, speaker, spans):
call["talk"][speaker] += speech_seconds(spans)
start, end = min(s for s, _ in spans), max(e for _, e in spans)
run = call["run"]
if run and run["speaker"] == speaker and start - run["end"] <= MONOLOGUE_GAP:
run["end"] = end # same person kept the floor
else:
run = call["run"] = {"speaker": speaker, "start": start, "end": end}
call["longest"][speaker] = max(call["longest"][speaker], run["end"] - run["start"])
def metrics(call, rep):
total = sum(call["talk"].values()) or 1.0
return {"talk_ratio": round(call["talk"][rep] / total, 2),
"rep_longest_monologue_s": round(call["longest"][rep]),
"talk_seconds": {k: round(v) for k, v in call["talk"].items()}}
def push_to_rep(bot_id, payload):
print(bot_id, payload) # send to the rep's side panel; never into the meeting chat
A few production notes. The rep's display name in the call may not match the CRM name, for example when they dial in by phone, so match it against the participant list. Pauses up to 0.5 s count as talking and gaps up to 1.5 s keep a monologue going; tune both on real calls. If the call ends mid-turn, flush the open spans when bot.stopped reaches your callback_url. With more than one server, move calls into Redis keyed by bot_id.
How does the LLM scoring loop keep up with a live call?
Run it on finished turns, not on every word, give it a short rolling window, and ask for structured output. Then put a rate limit between the model and the rep, because the scarce resource is the rep's attention, not tokens.
- Trigger. Score on the buyer's finished turns, where questions and objections show up, and debounce to one call every 20 to 30 seconds per meeting as a starting point.
- Input. The last six to ten finished turns, the running metrics, and deal context from your CRM via the deal ID in
custom_attributes. - Output. JSON with fields such as
objection,unanswered_question,next_step(owner, action, date) andnudge, which is empty most of the time. - Policy. One nudge on screen at a time, a few minutes between nudges, and each nudge expires after a minute. Log the turn that triggered it so a manager can see why it fired.
Use the calendar too. You know when the meeting is scheduled to end, so "no next step agreed, five minutes left" is a good first LLM nudge: easy to check and easy to act on. Latency is rarely the limit: MeetStream's live audio runs at about 200 ms, transcription and the LLM add seconds, and talk ratio and next steps move over minutes.
What should you write back to the CRM after the call?
Write the outcome, not the transcript: a short summary, next steps with owner and date, objections, and the call metrics, attached to the deal you passed in custom_attributes. Link to the full call in your own app.
Trigger the write on bot.stopped at your callback_url. With a streaming provider there is no post-call transcription.processed event, so build the record from the turns you already committed. For the recording, GET /api/v1/bots/{bot_id}/get_audio returns a presigned URL valid for one hour, so copy the file to your own storage, or have MeetStream write artifacts straight to your own S3 bucket. Per-participant files feed the post-call scorecard once processing finishes.
Lifecycle webhooks are best effort and are not retried on a non-2xx response, so poll GET /api/v1/bots/{bot_id}/status as a backstop. Show the draft CRM update in the rep's app for one-click approval before you write it. Reps fix wrong next steps faster than ops teams find them.
What consent and recording disclosure does live coaching need?
Tell everyone on the call that it is recorded and analysed, before it starts, and make the bot's purpose obvious from its name. Recording law differs by place, and calls cross state and national lines, so many teams give notice on every call. This is not legal advice.
In the US, the federal Wiretap Act allows recording when one party consents (18 U.S.C. 2511(2)(d)). Some states require more: California Penal Code 632 requires the consent of all parties to record a confidential communication, and Washington's RCW 9.73.030 requires all participants' consent while treating a recorded announcement as consent. In the EU, the GDPR needs a lawful basis for processing (Article 6) and requires telling people what you collect and why (Articles 13 and 14).
The product side of compliance:
- Name. Call the bot something like "Dana's notetaker (recording)", and post a notice with
bot_messagewhen it joins. - Notice. Show a "recording and analysing" card with
send_image, and add a line to the calendar invite. - Control. Give the rep one-click removal (
GET /api/v1/bots/{bot_id}/remove_bot) and pause or resume for sensitive moments (pause_recording,resume_recording). - Retention. Set
recording_config.retentionto{"type": "timed", "hours": N}, and delete on request withDELETE /api/v1/bots/{bot_id}/delete.
On Zoom, recording also waits for host consent. For security reviews: MeetStream is ISO 27001 certified, GDPR compliant with a data processing agreement and US and EU data residency, and HIPAA compliant with a BAA available; SOC 2 Type 2 is under audit. See security.
What does a coached hour cost?
On MeetStream's published pricing, a coached hour costs $0.45 in meeting infrastructure: $0.35 for the bot plus $0.10 for transcription, prorated to the second. Select startups and high-volume customers pay $0.25 for the bot, or $0.35 all in. Your LLM bill comes on top.
| Line item | Price | Notes |
|---|---|---|
| Bot in the call | $0.35 per hour | $0.25 for select startups and at volume; prorated to the second |
| Transcription | +$0.10 per hour | Add-on |
| Per-participant audio | Included | Off by default; set audio_separate_streams |
| Calendar integration | Free | Google and Microsoft calendars |
| Recording storage | 30 days included | Longer retention $0.05 per GB per month |
| Meeting infrastructure | $0.45 per coached hour | $0.35 at the startup and volume rate |
| LLM scoring | Your model bill | Calls per hour times tokens per call times your model's price |
To size the LLM line: if you score at most once every 30 seconds with about 2,000 input tokens each, that is up to 120 calls and 240,000 input tokens per coached hour. Multiply by your model's price per token. New accounts get $5 of free credit with no credit card, enough to run a pilot on real calls.
What should you build first?
Build in the order that earns the rep's trust. Reps forgive a missing feature faster than a wrong nudge in front of a buyer.
- Auto-join and talk ratio. Calendar auto-join, live transcript, talk ratio and longest monologue in the rep's panel. No LLM yet.
- Scorecard and CRM write-back. Built from the same committed turns, so managers get value even if reps ignore the panel.
- One LLM nudge. Missing next step near the end of the call.
- Post-call overlaps. Interruption analysis from per-participant audio, then more live nudges once reps trust the first one.
Everything above runs on one integration across Zoom, Google Meet and Teams. See the meeting bot API to start.
Frequently asked questions
Can a coaching bot whisper to the rep during a call?
No. There is no documented way for a meeting bot to play audio to one participant, and the meeting chat is visible to everyone. Deliver nudges in your own app: a side panel, overlay or second screen.
Is it legal to record sales calls for coaching?
Often, with notice and consent, but the rules vary. US federal law allows one-party consent, while states such as California and Washington require all parties, and the GDPR applies to people in the EU. Announce the recording on every call and get legal advice for your markets.
How accurate is talk ratio from a live transcript?
Accurate enough for coaching, because it changes over minutes. Live audio tags one dominant speaker per frame, so overlaps are approximate during the call. Per-participant audio is exact on Zoom and partial on Google Meet and Teams.
Does this work on Zoom, Google Meet and Microsoft Teams?
Yes. The create_bot call, the live transcript payload and the lifecycle events are the same on all three. Zoom needs a one-time Marketplace app setup and an OBF token for meetings outside your app's account.
How much does real-time sales coaching cost per hour?
On MeetStream's published pricing, $0.45 per coached hour for the bot and transcription, or $0.35 at the startup and volume rate. Add your LLM costs, which depend on how often you score.
About the author: Maddy is co-founder and CPO of MeetStream and leads product for the meeting bot API and its agent tools.
