[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build SMS OTP Verification

Build SMS OTP Verification

Build a secure SMS OTP verification system with rate limiting, delivery tracking, fallback channels, brute-force protection, and cost optimization through smart retry logic.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Marco leads engineering at a 20-person fintech app. They use email-based verification, but completion rates are 62% — users check email later or never. Fraud is increasing because email verification is easy to bypass. They want SMS OTP for critical actions (login, payment confirmation, account changes), but SMS costs $0.01-0.08 per message. Without rate limiting, a single attacker could cost them thousands. They also need fallback when SMS fails and protection against OTP brute-forcing.

Step 1: Build the OTP System

typescript
// src/auth/otp.ts — SMS OTP with rate limiting, brute-force protection, and fallback
import { pool } from "../db";
import { Redis } from "ioredis";
import { createHash, randomInt } from "node:crypto";

const redis = new Redis(process.env.REDIS_URL!);

interface OTPConfig {
  length: number;              // 4 or 6 digits
  expirySeconds: number;
  maxAttempts: number;         // max verification tries before lockout
  cooldownSeconds: number;     // minimum time between sends
  maxSendsPerHour: number;
  maxSendsPerDay: number;
}

const CONFIGS: Record<string, OTPConfig> = {
  login: { length: 6, expirySeconds: 300, maxAttempts: 3, cooldownSeconds: 60, maxSendsPerHour: 5, maxSendsPerDay: 10 },
  payment: { length: 6, expirySeconds: 180, maxAttempts: 3, cooldownSeconds: 30, maxSendsPerHour: 10, maxSendsPerDay: 20 },
  account_change: { length: 6, expirySeconds: 600, maxAttempts: 5, cooldownSeconds: 60, maxSendsPerHour: 5, maxSendsPerDay: 10 },
};

interface SendResult {
  success: boolean;
  error?: string;
  retryAfter?: number;         // seconds until can resend
  channel: "sms" | "whatsapp" | "email";  // actual delivery channel
  expiresIn: number;
}

// Send OTP
export async function sendOTP(
  phone: string,
  purpose: string,
  options?: { preferredChannel?: "sms" | "whatsapp" }
): Promise<SendResult> {
  const config = CONFIGS[purpose] || CONFIGS.login;
  const normalizedPhone = normalizePhone(phone);
  const rateLimitKey = `otp:rate:${normalizedPhone}`;

  // Check cooldown
  const lastSent = await redis.get(`otp:cooldown:${normalizedPhone}`);
  if (lastSent) {
    const elapsed = Date.now() - parseInt(lastSent);
    const remaining = Math.ceil((config.cooldownSeconds * 1000 - elapsed) / 1000);
    if (remaining > 0) {
      return { success: false, error: "Please wait before requesting another code", retryAfter: remaining, channel: "sms", expiresIn: 0 };
    }
  }

  // Check hourly/daily limits
  const hourKey = `otp:hourly:${normalizedPhone}:${Math.floor(Date.now() / 3600000)}`;
  const dayKey = `otp:daily:${normalizedPhone}:${new Date().toISOString().slice(0, 10)}`;

  const [hourCount, dayCount] = await Promise.all([
    redis.incr(hourKey).then(async (v) => { await redis.expire(hourKey, 3600); return v; }),
    redis.incr(dayKey).then(async (v) => { await redis.expire(dayKey, 86400); return v; }),
  ]);

  if (hourCount > config.maxSendsPerHour) {
    return { success: false, error: "Too many requests. Try again in an hour.", channel: "sms", expiresIn: 0 };
  }
  if (dayCount > config.maxSendsPerDay) {
    return { success: false, error: "Daily limit reached. Try again tomorrow.", channel: "sms", expiresIn: 0 };
  }

  // Generate OTP
  const code = generateOTP(config.length);
  const hashedCode = hashOTP(code, normalizedPhone);

  // Store OTP
  const otpKey = `otp:code:${normalizedPhone}:${purpose}`;
  await redis.setex(otpKey, config.expirySeconds, JSON.stringify({
    hash: hashedCode,
    attempts: 0,
    maxAttempts: config.maxAttempts,
    createdAt: Date.now(),
  }));

  // Set cooldown
  await redis.setex(`otp:cooldown:${normalizedPhone}`, config.cooldownSeconds, String(Date.now()));

  // Send via preferred channel with fallback
  let channel: "sms" | "whatsapp" | "email" = options?.preferredChannel || "sms";
  let delivered = false;

  // Try SMS first
  if (channel === "sms") {
    delivered = await sendSMS(normalizedPhone, `Your verification code is: ${code}. Expires in ${Math.floor(config.expirySeconds / 60)} min.`);
  }

  // Fallback to WhatsApp if SMS fails
  if (!delivered && channel === "sms") {
    channel = "whatsapp";
    delivered = await sendWhatsApp(normalizedPhone, code, config.expirySeconds);
  }

  // Last resort: email fallback
  if (!delivered) {
    channel = "email";
    const { rows: [user] } = await pool.query("SELECT email FROM users WHERE phone = $1", [normalizedPhone]);
    if (user?.email) {
      delivered = await sendEmailOTP(user.email, code, purpose, config.expirySeconds);
    }
  }

  if (!delivered) {
    return { success: false, error: "Could not deliver verification code. Please try again.", channel, expiresIn: 0 };
  }

  // Log for analytics
  await pool.query(
    `INSERT INTO otp_sends (phone_hash, purpose, channel, created_at) VALUES ($1, $2, $3, NOW())`,
    [createHash("sha256").update(normalizedPhone).digest("hex").slice(0, 16), purpose, channel]
  );

  return { success: true, channel, expiresIn: config.expirySeconds };
}

// Verify OTP
export async function verifyOTP(
  phone: string,
  purpose: string,
  code: string
): Promise<{ valid: boolean; error?: string; remainingAttempts?: number }> {
  const normalizedPhone = normalizePhone(phone);
  const otpKey = `otp:code:${normalizedPhone}:${purpose}`;

  // Check lockout
  const lockoutKey = `otp:lockout:${normalizedPhone}`;
  const locked = await redis.get(lockoutKey);
  if (locked) {
    const ttl = await redis.ttl(lockoutKey);
    return { valid: false, error: `Account locked. Try again in ${ttl} seconds.` };
  }

  const stored = await redis.get(otpKey);
  if (!stored) {
    return { valid: false, error: "Code expired or not found. Request a new one." };
  }

  const data = JSON.parse(stored);

  // Check attempts
  if (data.attempts >= data.maxAttempts) {
    await redis.del(otpKey);
    await redis.setex(lockoutKey, 900, "1"); // 15-minute lockout
    return { valid: false, error: "Too many failed attempts. Account locked for 15 minutes." };
  }

  // Verify hash
  const inputHash = hashOTP(code, normalizedPhone);
  if (inputHash !== data.hash) {
    data.attempts++;
    await redis.setex(otpKey, await redis.ttl(otpKey), JSON.stringify(data));
    return {
      valid: false,
      error: "Invalid code",
      remainingAttempts: data.maxAttempts - data.attempts,
    };
  }

  // Valid — clean up
  await redis.del(otpKey);
  await redis.del(`otp:cooldown:${normalizedPhone}`);

  // Log verification
  await pool.query(
    `INSERT INTO otp_verifications (phone_hash, purpose, attempts_used, verified_at) VALUES ($1, $2, $3, NOW())`,
    [createHash("sha256").update(normalizedPhone).digest("hex").slice(0, 16), purpose, data.attempts + 1]
  );

  return { valid: true };
}

// Generate cryptographically secure OTP
function generateOTP(length: number): string {
  const max = Math.pow(10, length);
  const min = Math.pow(10, length - 1);
  return String(randomInt(min, max));
}

// Hash OTP with phone as salt (prevents rainbow table attacks)
function hashOTP(code: string, phone: string): string {
  return createHash("sha256").update(`${code}:${phone}:${process.env.OTP_SECRET}`).digest("hex");
}

function normalizePhone(phone: string): string {
  return phone.replace(/[\s\-\(\)]/g, "").replace(/^00/, "+");
}

async function sendSMS(phone: string, message: string): Promise<boolean> {
  try {
    const res = await fetch("https://api.twilio.com/2010-04-01/Accounts/" + process.env.TWILIO_SID + "/Messages.json", {
      method: "POST",
      headers: {
        "Authorization": "Basic " + Buffer.from(`${process.env.TWILIO_SID}:${process.env.TWILIO_TOKEN}`).toString("base64"),
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: new URLSearchParams({ To: phone, From: process.env.TWILIO_FROM!, Body: message }),
    });
    return res.ok;
  } catch { return false; }
}

async function sendWhatsApp(phone: string, code: string, expirySeconds: number): Promise<boolean> {
  // WhatsApp Business API fallback
  try {
    const res = await fetch(`https://graph.facebook.com/v18.0/${process.env.WHATSAPP_PHONE_ID}/messages`, {
      method: "POST",
      headers: { "Authorization": `Bearer ${process.env.WHATSAPP_TOKEN}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        messaging_product: "whatsapp", to: phone,
        type: "template", template: {
          name: "otp_verification", language: { code: "en" },
          components: [{ type: "body", parameters: [{ type: "text", text: code }] }],
        },
      }),
    });
    return res.ok;
  } catch { return false; }
}

async function sendEmailOTP(email: string, code: string, purpose: string, expirySeconds: number): Promise<boolean> {
  await redis.rpush("email:send:queue", JSON.stringify({
    to: email, subject: `Your verification code: ${code}`,
    template: "otp_email", data: { code, purpose, expiresIn: Math.floor(expirySeconds / 60) },
  }));
  return true;
}

Results

  • Verification completion: 62% → 94% — SMS arrives in seconds; users verify without leaving the app; no more "check your email later"
  • Brute-force attacks neutralized — 3 wrong attempts = 15-minute lockout; hashed OTPs in Redis; no plaintext codes stored
  • SMS costs controlled — rate limiting caps at 10/day per number; an attacker can't trigger thousands of messages; monthly SMS cost stays under $200
  • Fallback prevents delivery failures — SMS fails → WhatsApp → email; 99.7% delivery rate across all channels; users always get their code
  • Fraud reduced 80% — payment confirmation requires OTP; stolen credentials alone aren't enough; chargebacks from account takeover dropped to near zero