Your own stack
You run the agent. Your carrier hands you the call, your code answers it. Here is where we fit, and the three shapes that takes.
Three shapes
Ask
Before your code answers, it asks us. One GET, a score from 0 to 1 with where we would draw the line on it, and you decide what to do. Nothing about your call path changes; you have added an if.
Tell
Your code reports what happened: a call started, a call ended, and if you have them, the turns in between. The start is scored on the spot and answers with the same score, so this is also asking. But now the call is on record, the alerts fire, and the caller goes on the network for everyone.
The gate
The carrier asks us before it asks you. We serve the carrier a few lines of TwiML or NCCO: hand the call straight to your agent, or first ask the caller to press a digit. A caller who cannot is never connected, and never costs you anything. Your code does not change at all.
Which to pick
| Ask | Tell | Gate | |
|---|---|---|---|
| A bad call costs you | nothing, if you refuse it | the call, unless you also ask | nothing: it is refused before it connects |
| Code change | one request where you answer | two requests per call | none; a setting at the carrier |
| Builds history | no | yes | yes |
| Alerts | no | yes | yes |
| Live monitor | no | yes, if you send turns | no |
| Needs | any key | full key or ingest token | a carrier number; Guard to challenge |
Most people who run their own stack do ask and tell together: ask when the call rings, tell when it starts and ends. If the number is on Twilio, Telnyx, SignalWire or Vonage, the gate does both for you and adds the challenge.
Ask and tell, together
import { murusai } from "./murusai.js"; // a thin fetch wrapper
// Your lines. 0.8 and 0.5 are where we would draw them; move either.
const REFUSE_AT = 0.8;
const VERIFY_AT = 0.5;
export async function onIncomingCall(call) {
// 1. Ask. Fail open: silence is worse than a spam call.
const s = await murusai.score({ caller: call.from, number: call.to }).catch(() => null);
const score = s?.score ?? 0;
if (score >= REFUSE_AT) return call.hangUp();
// 2. Tell. The start is scored and remembered; the answer is the same shape.
await murusai.event({ type: "call.started", call_id: call.sid, to: call.to, from: call.from });
// 3. Connect, and tell us when it's over.
const outcome = await connectToAgent(call, { verifyFirst: score >= VERIFY_AT });
await murusai.event({ type: "call.ended", call_id: call.sid, reason: outcome.reason, cost: outcome.cost });
}The score and the start can be the same request: a call.started answers with the score too. Ask separately only when you want the answer before anything is written down: a dry run for a number typed into a form, say.
What Guard means here
On a platform we connect to, Guard ends a bad call through the platform. On your own stack we have no handle on the call, so the acting is yours: you act on the score, and the gate refuses at the carrier. What Guard adds on your stack is the gate's challenge, the live monitor, texts and phone calls, unlimited numbers and the daily allowance.
call.suspicious from your own side.