Voice AI

SIMAgent: The SIM Card Is the Agent

Every data-plan alert you've ever received arrived after the damage was done. The network crossed your threshold sometime in the afternoon; the text landed at night, after you'd already streamed another episode. The most informed party in that transaction — the SIM, sitting directly on top of the usage counters — is the least empowered. It can't notice. It can't warn you. It can't do anything about it.

SIMAgent flips that. It's a TypeScript agent that runs on the Telnyx Edge runtime and embodies a single SIM card: it tracks data usage, wakes when thresholds are breached, texts the owner before the overage hits, answers plan questions in plain language with an LLM, and — on confirmation — upgrades the plan through the Telnyx Wireless API on its own. It even answers inbound calls with a spoken usage history.

The tagline is the architecture: the actor IS the SIM. In this walkthrough we'll cover what it does, how it's built on Telnyx's AI Communications Infrastructure, and how to run it yourself in about five minutes — entirely in demo mode, with no real SMS, calls, or provisioning charges.

What the App Does

SIMAgent is a single durable agent (SIMAgent extends Agent) bound to one SIM. Over its lifecycle it:

  • Ingests usage. Telnyx Wireless data-usage webhooks hit POST /webhooks/usage; the agent folds each delta into durable state.
  • Watches thresholds on a schedule. A recurring schedule compares usage against the plan. At 80% it sends a proactive SMS; the alert flags are stored so each threshold fires exactly once per cycle.
  • Answers questions. Inbound customer SMS hits POST /webhooks/sms, and the agent calls Telnyx Inference to produce a natural-language comparison of available plans — sized to fit in a text message.
  • Provisions upgrades. When the customer confirms, the agent calls the Telnyx Wireless API (POST /v2/sim_cards/{id}) to raise the data limit, then texts a confirmation.
  • Resets on the billing boundary. A 30-day schedule zeroes the counters and texts a cycle summary.
  • Takes calls. Inbound calls are answered via Call Control, and the agent speaks the usage history back using text-to-speech.

How It Works

One agent, three bindings?

The agent declares three [telnyx] bindings in telnyx.toml — Messaging for SMS, Voice for Call Control, and Wireless for SIM provisioning — plus an Inference binding for the LLM. That's the core bet of Telnyx's AI Communications Infrastructure: messaging, voice, wireless, and inference are one API surface, so a single agent can hold all four capabilities without gluing together four separate integrations.

How does the agent keep state without a database?

Usage counters, the current plan, and alert flags live in durable agent state:

async onUsageWebhook(event: UsageEvent) {
  const state = await this.getState<SimState>();
  await this.setState({
    ...state,
    usageBytes: state.usageBytes + event.deltaBytes,
  });
  await this.events.emit("usage.recorded", { deltaBytes: event.deltaBytes });
}

this.getState() / this.setState() persist across restarts and cold starts — there's no external database to provision. this.events gives you a replayable progress log, which doubles as the audit trail, and this.messages keeps the per-SIM conversation history.

How do schedules survive cold starts?

Two durable schedules drive the proactive behavior:

await this.every(this.env.USAGE_CHECK_SECONDS ?? 3600, "check-usage");
await this.every(this.env.BILLING_CYCLE_SECONDS ?? 2_592_000, "billing-reset");

The threshold check compares usage to plan and sends an SMS through the Messaging binding when a threshold is first crossed:

if (pct >= 0.8 && !state.alerted["80"]) {
  await this.env.TELNYX.messages.send({
    from: this.env.TELNYX_SMS_FROM_NUMBER,
    to: state.ownerNumber,
    text: `⚠️ You're at ${Math.round(pct * 100)}% of your ${state.planName} plan. Reply PLANS to compare upgrades.`,
  });
  await this.setState({ ...state, alerted: { ...state.alerted, "80": true } });
}

Because schedules are durable and re-armed on activation, a cold start doesn't lose the billing-cycle timer.

How are inbound webhooks trusted?

In live mode, every inbound webhook is verified with Ed25519 signatures before the agent acts on it:

const event = await telnyx.webhooks.unwrap(request, {
  publicKey: this.env.TELNYX_PUBLIC_KEY,
});

This is the trust layer of the pattern: the agent only mutates its own state — and only provisions real plan changes — on webhooks Telnyx signed.

How does the LLM stay grounded?

When a customer texts "should I upgrade?", the agent calls Telnyx Inference with the SIM's actual usage context:

const completion = await this.env.TELNYX.ai.openai.chat.createCompletion({
  model: "zai-org/GLM-5.2",
  messages: [
    { role: "system", content: PLAN_COMPARISON_PROMPT },
    { role: "user", content: buildUsageContext(state) + question },
  ],
});

The system prompt constrains output to short, SMS-sized comparisons grounded in the SIM's real numbers — no hallucinated plans, no paragraphs.

How does the same state drive a phone call?

Inbound calls go through Call Control. The agent answers, pulls the durable usage history, and speaks it back:

await fetch(`https://api.telnyx.com/v2/calls/${callControlId}/actions/answer`, { ... });
await fetch(`https://api.telnyx.com/v2/calls/${callControlId}/actions/speak`, {
  method: "POST",
  body: JSON.stringify({ payload: usageSummary(state), voice: "female", language: "en-US" }),
});

One actor, one memory, every channel.

The data flow, end to end

  1. Telnyx usage webhook → /webhooks/usage → durable usage state updated
  2. Schedule wakes → getState() → usage ≥ 80% → SMS via the Messaging binding
  3. Customer SMS → /webhooks/sms → LLM plan comparison → SMS response
  4. Customer confirms upgrade → TELNYX.simCards.update() → SMS confirmation
  5. Billing boundary → schedule fires → state reset → SMS summary
  6. Customer calls → Call Control answer → usage history → text-to-speech

Setup

# 1. Clone the repo
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/sim-agent

# 2. Copy the example env file and fill in your credentials
cp .env.example .env

# 3. Install dependencies
npm install

# 4. Typecheck and build
npm run typecheck && npm run build

# 5. Start the agent locally (telnyx-edge dev)
npm start

# 6. Run the self-contained smoke test
npm run smoke

You'll need three values from the Telnyx Portal: TELNYX_API_KEY (API Keys), TELNYX_PUBLIC_KEY (Credentials — required for live-mode webhook verification), and TELNYX_SMS_FROM_NUMBER (Numbers). DEMO_MODE=true is the default, so the smoke test runs end-to-end without touching real SIMs. USAGE_CHECK_SECONDS and BILLING_CYCLE_SECONDS let you compress the schedule intervals for testing.

The local API surface is small: POST /api/sim initializes the agent for a SIM, POST /api/usage records a usage delta, POST /api/demo runs the full flow end to end, and GET /api/sim returns current state and schedules. If the plan comparison ever returns canned text, check the model name and that the [telnyx] Inference binding is declared in telnyx.toml — that's the most common misconfiguration.

Where to Take It Next

The pattern generalizes beyond SIMs. Any network endpoint with state — a number, a trunk, a room, a device — can be modeled as a durable agent that watches its own telemetry, talks on its own behalf, and takes provisioning action when authorized. SIMAgent is the reference implementation of that idea on Telnyx's AI Communications Infrastructure: the entity that generates the events is the entity that responds to them.

Clone the repo, run the smoke test, and give your SIM a voice. Then tell us what endpoint you'd turn into an agent next.

Ready to build with low-latency voice AI?

Join developers building the future of real-time conversations