Back to Explore
GuideVibeFix 编辑部Updated Oct 13, 2026

Don't Let SMS OTP Sink You: Login and Anti-Abuse Playbook for Vibe Projects

SMS OTP is the fastest login to ship — and the easiest to get burned by. This guide covers the three pits (cost, SMS bombing abuse, template compliance), a production-ready code/verify flow with rate limits, layered anti-abuse defenses, cost controls, and when to switch to email OTP or social login. Includes a launch checklist and copy-pasteable verification logic.

Editorial illustration of SMS verification login security: phone with one-time passcode and shield motif, representing OTP anti-abuse practices

Almost every vibe project reaches this moment: users say "let me log in with my phone number, it's convenient." You have your AI agent wire up an SMS provider, it's working in half an hour, the test message arrives — "Your verification code is 482913" — and the dopamine hits. Then the first week's bill arrives: hundreds in SMS fees — for 40 registered users.

SMS one-time passcodes are login's "sweet trap": the lowest barrier to ship, the most ways to get burned. This guide covers the three real pits (cost, abuse, compliance) and gives you a production-ready send/verify flow, layered anti-abuse defenses, and cost controls. One goal: turn SMS OTP from a ticking bomb into a controlled cost line.

Pit 1: cost — pennies per message, until someone spends them for you

Do the math first. A domestic OTP SMS costs roughly 3–5 cents; sounds cheap. But note two details: first, OTP user behavior is "didn't get it, hit resend" — a normal login can trigger 2–3 messages; second, once your endpoint is abused, an attacker can burn thousands of messages overnight — real money, hundreds of dollars. For a side project making a few thousand a month, one abuse incident can eat the entire month's profit.

The sneakier cost is deliverability. A share of OTP messages never arrive (dead numbers, ported numbers, carrier filtering), and many plans bill you anyway. So when choosing a provider, don't just compare unit price — ask three questions: are failed sends billed, are there spike alerts, and are there test numbers/templates that don't bill? Use whitelisted test numbers during development, and never integration-test with real phone numbers — plenty of people have burned tens of dollars in a day of testing.

Pit 2: SMS bombing — your endpoint is someone else's weapon

This is the most dangerous pit. Attackers don't need to breach your system; they just hammer your "send verification code" endpoint to bombard someone else's phone number: a loop script turns your server into a free SMS bombing service. Victims report it, and it's your SMS signature that gets banned; carriers flag you for spam — throttled at best, service suspended at worst.

The core defense is one sentence: before sending a code, prove "you're human, and you actually own this number." In engineering terms, that's four layers of depth:

  • Layer 1: rate limits (mandatory). Same phone number: 1 send per 60 seconds, max 5–10 per day; same IP: max N requests per hour (N depends on your traffic; 20–30 is a sane starting point). Keep IP limits loose — corporate/school egress IPs are shared, and strict limits will hit legitimate users.
  • Layer 2: bot check before sending. Put a CAPTCHA or behavioral check (slider, invisible verification) in front of the "send code" button. Best bang-for-buck anti-abuse there is: scripts hate this one extra step. Yes, it costs some UX — but "one extra swipe" versus "thousands of messages burned at 3 AM" is an easy trade.
  • Layer 3: cooldowns and attempt caps. Codes valid 5–10 minutes; a new code invalidates the old one immediately; max 5 verification attempts, then lock for 30 minutes. This stops brute-forcing 6-digit codes.
  • Layer 4: anomaly monitoring and alerts. Track sends hourly; on a spike (say, your normal tens per hour suddenly becoming thousands), alert immediately and auto-tighten limits. This is the last fuse, and it has saved many people's bacon.

Pit 3: compliance — in China, one wrong character in the template and nothing sends

Chinese SMS is a different world from the rest of the planet. Signatures must be pre-registered (usually your company, product, or trademark name), every template must be individually approved, typically in the format "Your verification code is {code}, valid for 5 minutes." Approval takes 1–2 business days — so apply for SMS capability early, not the day before launch.

Field notes: keep signatures short — characters inside count toward message length, and anything over 70 characters bills as two segments; use explicit variable markers like {code}, and never mix marketing copy into OTP templates — that's heavily penalized; if you serve overseas users, note that international SMS runs on global routes at 10x+ domestic prices, which is when the alternatives below deserve serious thought.

A launch-ready flow

Fill in all the pits above and the standard send/verify flow looks like this (pseudocode, state in Redis):

// Send verification code
async function sendSmsCode(phone, ip, captchaToken) {
// 1. Bot check first
if (!await verifyCaptcha(captchaToken)) throw new Error("Bot check failed");
// 2. Rate limits: phone 1/60s, 10/day; IP 30/hour
await checkRateLimit(`sms:phone:${phone}`, 60, 1);
await checkRateLimit(`sms:phone:day:${phone}`, 86400, 10);
await checkRateLimit(`sms:ip:hour:${ip}`, 3600, 30);
// 3. 6-digit code from a CSPRNG — never Math.random()
const code = cryptoRandom6Digits();
// 4. Store only the hash in Redis, never plaintext; 5-min TTL
await redis.setex(`otp:${phone}`, 300, sha256(code));
await redis.setex(`otp:attempts:${phone}`, 300, 0);
// 5. Send via provider; log failures into monitoring
await smsProvider.send(phone, templateId, { code});
}

// Verify code
async function verifySmsCode(phone, inputCode) {
const attempts = await redis.incr(`otp:attempts:${phone}`);
if (attempts > 5) throw new Error("Too many attempts, try again in 30 minutes");
const hash = await redis.get(`otp:${phone}`);
if (!hash || hash!== sha256(inputCode)) throw new Error("Invalid or expired code");
// Delete on success: one code, one use
await redis.del(`otp:${phone}`, `otp:attempts:${phone}`);
return true;
}

Details AI agents commonly get wrong — check these in review: codes must come from a cryptographically secure RNG (Math.random is predictable); store hashes in Redis, never plaintext (and never log plaintext codes); delete immediately after successful verification — one code, one use; a new code invalidates the old (the setex overwrite above handles this); and don't distinguish "phone not registered" from "wrong code" in error messages — return a uniform "invalid code" so attackers can't enumerate registered users.

Cost control: four moves to squeeze the SMS bill

  1. Fixed test codes in non-production: intercept real sends in dev/test and return a fixed code (e.g. 123456), configurable in the admin panel. This single move saves the most.
  2. Reuse login sessions; don't re-send on every open: get access token + refresh token right, with 30-day sessions. A big chunk of many projects' SMS bills is "user gets a code every time they open the app" — pure design flaw.
  3. Email OTP first, SMS later: email codes cost near zero. For non-strict-identity scenarios, default to email login with SMS as an option — that alone can cut 80%+ of SMS volume.
  4. Monitoring + budget alerts: set a monthly budget alert on the SMS provider account and push a daily send-volume digest to your phone. Discovering abuse an hour earlier is hundreds saved.

When to drop SMS: choosing alternatives

SMS OTP is not mandatory. Choose by scenario:

  • Chinese consumer app needing real identity or max conversion: SMS OTP still converts best; worth the anti-abuse investment.
  • Mostly overseas users: international SMS is pricey with shaky deliverability — prefer email OTP + social login (Google/Apple).
  • Developer tools / geek products: GitHub login does the job; users may even find phone-number entry "heavy."
  • Inside the WeChat ecosystem: WeChat one-tap / phone-number quick login beats self-built SMS on both UX and cost.

My take: an indie developer's default should be "email OTP + one social login," not SMS. Add SMS only after you've validated that users actually need it. Many projects wire up SMS on day one purely because "the AI agent said it's standard practice" — standard practice is written for big companies with fraud teams. You don't have one.

A typical blow-up timeline (count how many apply to you)

These incidents follow a strikingly similar script — worth checking yourself against: Day 1, the agent wires up SMS with zero rate limiting and ships; Day 3, someone posts on a forum that "this site's code endpoint has no limits," and a scripter writes a 20-line loop; Day 4, 3 AM, your SMS provider's risk control kicks in and your signature is suspended — real users registering right now get no codes and start complaining; Day 5, the bill: 20,000 messages in 3 days, 90% from the attacker. The fix took half a day (the four layers above); getting the signature unsuspended took a week; the lost user trust never came back.

The most expensive part of that script was never the SMS bill — it was the week of real-user churn while the signature was suspended. So anti-abuse isn't "add it after you're attacked"; it ships the same day as the SMS endpoint. Remember the order: rate limits and bot checks go in version one, not in the TODO list.

Launch checklist: tick every box before shipping

  • □ SMS signature and templates approved; production template IDs configured
  • □ Phone-level limits live (1/60s, 10/day) plus IP-level limits
  • □ Bot check (CAPTCHA/slider/invisible) before sending
  • □ Codes from CSPRNG, hashes in Redis, 5–10 min TTL, one code one use
  • □ 5 failed attempts → lockout; errors don't reveal "user not found"
  • □ Non-production intercepts real sends with fixed test codes
  • □ Spike alerts configured (threshold at 5–10x normal volume)
  • □ Monthly budget alert set on the provider account
  • □ No plaintext codes in logs

One last restatement of this guide's core judgment: the risk of SMS OTP was never "can you wire it up" — it's "what happens after launch." Your AI agent can get the integration working in half an hour, but anti-abuse, rate limiting, alerting, and compliance are "after launch" concerns the agent won't raise on its own. Spend an hour on the checklist above before shipping — it might save your project's entire first month of profit.

Views 1Comments 0

Comments (0)

ME
0/1000
Loading comments...
Browse projectsPublish your project

Related articles

API key creation form showing key name, associated user, permissions, rate limit override and expiry fields
Guide
Stop Handing Out Master Keys: Customer-Facing API Key Issuance & Scopes for Vibe Projects

Opening up your API means issuing keys to users — and most solo teams get it wrong: plaintext storage, one god-key for everything, no revocation, no usage attribution. This guide gives you a copy-paste-ready system: key format design, hash-only storage, scope permission models, rotation/revocation SOPs, and per-key rate limiting with usage attribution — with TypeScript templates, Prisma schemas, and a one-week rollout checklist.

Payments & MonetizationBackend EngineeringAuthentication & Access
Concept illustration of the three-layer moderation funnel for solo teams: machine pre-review, user reports, and human final review
Guide
Content Moderation for Solo Teams: A Three-Layer Funnel, 30 Minutes a Week

If your vibe project has anywhere users can type, you need moderation — and it isn't 'deleting posts,' it's tiered triage. This guide maps the three risk surfaces (traditional UGC, user-publishable content, AI-generated output), then builds the funnel: machines do 90% with a keyword-plus-API stack, user reports act as sensors with tiered SLAs, and you spend 30 minutes a week on the 1% of hard cases. Copy-ready guidelines template and disaster breakers included.

Tool TipsProject BuildingIndie Development
Diagram of edge-first AI architecture: inference running on the user's device with cloud as fallback
Guide
Push AI Compute to the User: Local/Edge Inference for Vibe Projects

The cheapest GPU is your user's GPU. This guide shows indie teams how to push suitable inference workloads to the browser and edge — WebGPU, transformers.js, WebLLM, llama.cpp, Ollama, Apple MLX compared; 1B–8B quantized models mapped to tasks; an edge-first degradation chain with drop-in fallback code; packaging "data never leaves the device" as a selling point; hard limits on load size, memory and heat; and a two-week pilot SOP. Cloud stays only as the safety net.

DeploymentTool ComparisonIndie Development