---
name: sms-two-factor-agent
title: "SMS Two-Factor Authentication Agent"
description: "Agent-managed SMS two-factor authentication with code generation, KV storage, and scheduled expiry."
language: typescript
framework: edge
telnyx_products: [SMS, Verify, Agent SDK]
---
# SMS Two-Factor Authentication Agent
An Edge-based agent that manages the full lifecycle of SMS two-factor authentication codes — generation, delivery via Telnyx SMS, verification, and automatic expiry using scheduled tasks.
## Why Telnyx
Telnyx provides the AI Communications Infrastructure that powers programmable SMS, voice, and verification workflows with low-latency global delivery. By combining the Telnyx Edge Agent SDK with the native `[telnyx]` binding, this sample demonstrates zero-credential API access to Telnyx messaging services directly from the edge runtime, enabling secure, scalable authentication flows without managing API keys in application code.
## Telnyx API Endpoints Used
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/v2/messages` | POST | Send SMS verification codes via `this.env.TELNYX.messages.send()` |
| Telnyx Edge `[telnyx]` binding | — | Zero-credential access to Telnyx messaging from Edge runtime |
| Telnyx KV (`[storage.kv.KV]`) | — | Code store with `expirationTtl` — keys auto-expire |
## Architecture
```
┌──────────────┐ POST /verify ┌──────────────────────────┐
│ Client │ ────────────────────► │ TwoFactorAgent │
│ (Web/App) │ │ (extends Agent) │
└──────────────┘ │ one actor per phone │
└────────────┬─────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ KV │ │ Agent state │ │ Telnyx SMS │
│ (TTL │ │ (attempts + │ │ (via the │
│ 300s) │ │ rate limit)│ │ [telnyx] │
└────┬────┘ └─────────────┘ │ binding) │
│ └──────┬──────┘
│ │
│ ┌──────────────────────┘
│ │
▼ ▼
┌─────────────────────┐
│ User Phone │
│ receives code │
└──────────┬──────────┘
│
▼
POST /check
│
▼
┌─────────────────────────┐
│ TwoFactorAgent │
│ verify against KV → │
│ cleanup on success → │
│ schedule() expiry net │
└─────────────────────────┘
```
**Flow:**
1. Client requests verification for a phone number (`POST /verify`)
2. The fetch handler routes to the phone's durable actor (`idFromName`)
3. Agent rate-limits via durable per-phone state (5 attempts per 5-minute window), generates a 6-digit code, and stores it in KV with `expirationTtl: 300`
4. Agent sends the SMS via the zero-credential `[telnyx]` binding (`this.env.TELNYX.messages.send()`)
5. Agent schedules `expireCode` cleanup via `this.schedule(300, "expireCode", { phone })` as a safety net
6. User receives the SMS and submits the code (`POST /check`)
7. Agent verifies against KV; success clears the code and resets counters; failure increments the fail counter
Note: KV keys allow only `a-z A-Z 0-9 - _ / = .`, so the E.164 `+` is stripped — the key for `+17177247292` is `2fa/17177247292`.
## Environment Variables
| Variable | Type | Example | Required | Description | Where to get it |
|----------|------|---------|----------|-------------|-----------------|
| `TELNYX_API_KEY` | `string` | `your_telnyx_api_key_here` | **yes** | Telnyx API key — injected automatically by the `[telnyx]` binding; also used by the `telnyx-edge` CLI | [Telnyx Portal → API Keys](https://portal.telnyx.com) |
| `DEMO_MODE` | `string` | `true` / `false` | no | `true` (default) logs codes to the actor console instead of sending SMS; `false` sends real SMS | set in `telnyx.toml` `[env_vars]` |
| `TELNYX_FROM_NUMBER` | `string` | `+16282564655` | no (live mode) | SMS-capable sender number in E.164 | buy a number at [telnyx.com](https://telnyx.com/products/number-api) |
> **Agent / CLI access** — all of the above can be provisioned from the CLI/agent without the portal:
>
> ```bash
> telnyx auth set-key KEY… # human CLI auth (or TELNYX_API_KEY env var for agents)
> telnyx number-orders create --profile international --quantity 1 # buy an SMS-capable number
> telnyx-edge storage kv create --name sms-two-factor-agent-2fa # provision the KV namespace
> ```
## Setup
### Prerequisites
- Node.js 18+ and npm
- Docker (compose plugin) — for `telnyx-edge dev`
- A Telnyx account with an SMS-capable number (10DLC campaign required for US A2P traffic)
- Telnyx Edge CLI: install from [github.com/team-telnyx/edge-compute/releases](https://github.com/team-telnyx/edge-compute/releases)
### Local Development
```bash
# Clone the repository
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/sms-two-factor-agent
# Authenticate the Edge CLI (or export TELNYX_API_KEY)
export TELNYX_API_KEY=your_telnyx_api_key_here
# Install dependencies
npm install
# Typecheck + smoke test (loads the module, verifies the Agent contract)
npm run typecheck
npm test
```
Programmatic / CLI setup
```bash
# Create the StatefulActor function (registers func_id with the platform)
telnyx-edge new-func --actor -l ts -n sms-two-factor-agent
# → copy the printed func_id into telnyx.toml [edge_compute]
# Provision the KV namespace for the codes
telnyx-edge storage kv create --name sms-two-factor-agent-2fa
# → copy the KV ID into telnyx.toml [storage.kv.KV] id
# Ship to Telnyx Edge (~5-10 min: upload, build, deploy)
telnyx-edge ship
# Deployed URL is printed at the end; also visible via:
telnyx-edge list
```
### Deploy and Run
```bash
# Ship to Telnyx Edge Compute
telnyx-edge ship
# Health check
curl https://.telnyxcompute.com/health
# Demo mode (default): codes are logged to the actor console — no SMS sent
# Live mode: set DEMO_MODE = "false" in telnyx.toml [env_vars], then re-ship
# Send a real verification SMS
curl -X POST https://.telnyxcompute.com/verify \
-H "Content-Type: application/json" \
-d '{"phone": "+17177247292"}'
# Verify the code the user received
curl -X POST https://.telnyxcompute.com/check \
-H "Content-Type: application/json" \
-d '{"phone": "+17177247292", "code": "928723"}'
```
**Important:** `[env_vars]` in `telnyx.toml` are injected into the **function runtime's** `process.env` only — the actor runtime has its own empty `process.env`. The fetch handler therefore passes `DEMO_MODE` and `TELNYX_FROM_NUMBER` into `sendCode()` explicitly. Do not read those env vars directly inside the agent class.
### Project Structure
```
sms-two-factor-agent/
├── src/
│ └── index.ts # Main entry — fetch front door + TwoFactorAgent
├── telnyx.toml # Edge manifest — actors, [telnyx], KV, env vars
├── telnyx-env.d.ts # Generated binding types (telnyx-edge types)
├── package.json
├── tsconfig.json
├── smoke_test.ts
├── .env.example
├── .gitignore
├── README.md
├── API.md
└── GUIDE.md
```
## API Reference
### POST `/verify`
Initiate SMS two-factor authentication for a phone number.
**Request:**
```json
{
"phone": "+15551234567"
}
```
**Response (200):**
```json
{
"ok": true,
"message": "Verification code sent. Check your phone.",
"demo_mode": false,
"message_id": "4031a083-1c0b-4fee-a298-e2249ef1f421"
}
```
**Response (400):**
```json
{ "error": "A valid E.164 phone number is required (e.g. +15551234567)" }
```
**Response (429):**
```json
{ "error": "Too many attempts. Please try again later." }
```
---
### POST `/check` (alias: `POST /verify/code`)
Verify the SMS code submitted by the user.
**Request:**
```json
{
"phone": "+15551234567",
"code": "123456"
}
```
**Response (200):**
```json
{
"verified": true,
"message": "Phone number verified."
}
```
**Response (401):**
```json
{
"verified": false,
"error": "Invalid code",
"status": "invalid",
"fails_remaining": 4
}
```
**Response (404):**
```json
{ "error": "No active verification code. Request a new one." }
```
---
### GET `/health`
Health check endpoint.
**Response (200):**
```json
{
"status": "ok",
"agent": "TwoFactorAgent"
}
```
## Troubleshooting
| Issue | Cause | Solution |
|-------|-------|----------|
| `ok: true` but no SMS received in live mode | `[env_vars]` are injected into the function runtime only — the actor runtime's `process.env` is empty, so the agent silently took the demo branch | The fetch handler passes `DEMO_MODE`/`TELNYX_FROM_NUMBER` explicitly into `sendCode()`; never read these env vars inside the agent class |
| KV `HTTP 400: Invalid key format` | KV keys allow only `a-z A-Z 0-9 - _ / = .` — the E.164 `+` (and `:`) are rejected | `kvKey()` sanitizes the phone; keep the `2fa/` prefix format |
| `KV put ... failed: HTTP 500` | Telnyx KV write-path outage (reads can stay healthy) | The agent falls back to the actor's durable storage (`ctx.storage`) automatically; retry later |
| Rate limit triggered | 5 send-code attempts in the 5-minute window | The window resets when the code expires (`expireCode` task) or on successful verification — request a new code after 5 minutes |
| `schedule()` not firing | Tasks survive restarts but need a live deployment | Verify `TwoFactorAgent` is registered via `[[actors]]` in `telnyx.toml` and the function is `deploy_ok` |
| `reset-func` stuck in `resetting` | Platform-side operation backlog | Retry `telnyx-edge reset-func --yes` after the current operation clears, or ship directly from `deploy_failed` |
| 409 `Function Busy` on ship | A ship/delete/reset operation is already in progress | Wait for the in-flight operation to finish (`telnyx-edge list`), then re-ship |
## Agent Discovery
This example is part of the [Telnyx Code Examples](https://github.com/team-telnyx/telnyx-code-examples) catalog.
- **Agent signup**: [telnyx.com/agent-signup.md](https://telnyx.com/agent-signup.md) — automated account provisioning via agent mail; get an API key with no human intervention
- **Agent CLI**: [github.com/team-telnyx/ai/tree/main/cli](https://github.com/team-telnyx/ai/tree/main/cli) — composite commands for agents ([commands reference](https://github.com/team-telnyx/ai/tree/main/cli/src/commands))
- **Agent skills**: [github.com/team-telnyx/ai/tree/main/skills](https://github.com/team-telnyx/ai/tree/main/skills)
- **Telnyx AI repo**: [github.com/team-telnyx/ai](https://github.com/team-telnyx/ai)
- **LLM-optimized docs**: [`llms-full.txt`](https://developers.telnyx.com/llms-full.txt)
- **Example index**: [`llms.txt`](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/llms.txt)
- **Telnyx CLI (human)**: [developers.telnyx.com/development/cli](https://developers.telnyx.com/development/cli) — `go install github.com/team-telnyx/telnyx-cli/cmd/telnyx@latest`
## Related Examples
- [agent-sms-triage-bot](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/agent-sms-triage-bot/README.md) — Inbound SMS triage with a scheduled agent
- [edge-customer-agent-typescript](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/edge-customer-agent-typescript/README.md) — Durable entity agent per phone number (StatefulActors deep-dive)
- [edge-event-microsite](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/edge-event-microsite/README.md) — KV-backed event site with an SMS/WhatsApp concierge
## Resources
- [Stateful Actors Quick Start](https://developers.telnyx.com/docs/edge-compute/stateful-actors/quick-start)
- [Send SMS Guide](https://developers.telnyx.com/docs/messaging/send-sms)
- [SMS API Reference](https://developers.telnyx.com/api-reference/sms)
- [Telnyx Messaging Product](https://telnyx.com/products/sms-api)
- [Telnyx Verify Product](https://telnyx.com/products/verify)
- [Telnyx Pricing](https://telnyx.com/pricing)
- [Edge Compute CLI](https://github.com/team-telnyx/edge-compute/releases)
- [Telnyx Developer Docs](https://developers.telnyx.com)