[{"data":1,"prerenderedAt":1688},["ShallowReactive",2],{"page-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fpreventing-free-tier-abuse\u002F":3,"faq-schema-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fpreventing-free-tier-abuse\u002F":1667},{"id":4,"title":5,"body":6,"description":1656,"extension":1657,"meta":1658,"navigation":631,"path":1663,"seo":1664,"stem":1665,"__hash__":1666},"content\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fpreventing-free-tier-abuse\u002Findex.md","Preventing Free-Tier Abuse",{"type":7,"value":8,"toc":1642},"minimark",[9,13,23,26,31,39,51,159,166,168,172,175,291,296,317,321,338,342,345,367,375,378,456,552,554,558,569,1342,1351,1444,1458,1460,1464,1486,1493,1495,1499,1510,1518,1520,1524,1527,1529,1533,1539,1545,1551,1574,1584,1588,1593,1619,1624,1638],[10,11,5],"h1",{"id":12},"preventing-free-tier-abuse",[14,15,16,17,22],"p",{},"A free tier is a customer acquisition channel until it becomes a denial-of-wallet attack. The moment your API is worth scripting against, someone will spin up a thousand throwaway accounts to farm your quota, your compute, or your upstream third-party credits. Part of the ",[18,19,21],"a",{"href":20},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002F","Designing API Pricing Tiers"," guide, this page walks through layered defenses — identity verification, per-account and per-IP limits, card-on-file, quota plus velocity checks, and disposable-email blocking — and ships a runnable FastAPI guard that enforces a free-tier quota and IP velocity using Redis. The goal is not to make abuse impossible; it is to make abuse cost the attacker more than it costs you, without taxing the honest developer who is your future paying customer.",[24,25],"hr",{},[27,28,30],"h2",{"id":29},"the-threat-account-farming-and-denial-of-wallet","The threat: account farming and denial of wallet",[14,32,33,34,38],{},"Free-tier abuse is rarely one bad actor making too many calls. It is one bad actor making ",[35,36,37],"em",{},"many accounts",", each staying just under your per-account limit. Sum those accounts and your free tier is now your most expensive cost center, often funded by your own metered upstream APIs. Defenses fall into two buckets: raising the cost of creating an account (identity), and capping what any identity — and any network origin — can do (rate and quota). You need both. Identity alone is beaten by automation; limits alone are beaten by volume of accounts.",[14,40,41,42,45,46,50],{},"The cost of getting this wrong is direct and it compounds. Every abusive free call burns the same compute and egress you priced into your paid tiers in the ",[18,43,44],{"href":20},"pricing tiers guide",", and if your endpoints proxy a paid third-party API, abuse converts straight into a bill you did not invoice. The math is unforgiving when a language model sits behind the endpoint. Suppose each free account farms its full 1,000-call allowance and each call costs you $0.002 of upstream model spend — a realistic figure from ",[18,47,49],{"href":48},"\u002Fautomating-side-hustle-operations-with-apis\u002Fautomating-ai-workflows-with-python-apis\u002Fcontrolling-llm-api-costs-in-production\u002F","controlling LLM API costs in production",". Ten farmed accounts is a rounding error at $20. Ten thousand farmed accounts, which a single script can register overnight against an undefended signup, is $20,000 of pure loss against zero revenue. That is denial of wallet: the attacker spends nothing and you get invoiced by your own vendors.",[52,53,61,62,61,66,61,70,61,77,61,85,61,91,61,99,61,107,61,113,61,118,61,124,61,129,61,132,61,138,61,143,61,146,61,151,61,156],"svg",{"viewBox":54,"role":55,"ariaLabelledBy":56,"xmlns":59,"style":60},"0 0 720 320","img",[57,58],"fta-cost-t","fta-cost-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[63,64,65],"title",{"id":57},"Upstream cost of unchecked account farming",[67,68,69],"desc",{"id":58},"Bar chart showing monthly upstream loss growing from twenty dollars at ten farmed accounts to twenty thousand dollars at ten thousand accounts, each account farming a thousand calls at two-tenths of a cent each.",[71,72],"rect",{"x":73,"y":73,"width":74,"height":75,"fill":76},"0","720","320","var(--c-surface)",[78,79,84],"text",{"x":80,"y":81,"fill":82,"style":83},"20","30","var(--c-text)","font-size:14;font-family:var(--font-sans);","Monthly loss to farming (1,000 calls\u002Faccount at $0.002)",[78,86,90],{"x":80,"y":87,"fill":88,"style":89},"48","var(--c-text-muted)","font-size:11;font-family:var(--font-sans);","bar height on a log scale of dollars lost",[92,93],"line",{"x1":94,"y1":95,"x2":96,"y2":95,"stroke":97,"style":98},"70","270","700","var(--c-border)","stroke-width:1;",[71,100],{"x":101,"y":102,"width":103,"height":104,"rx":105,"fill":106},"110","220","90","50","3","var(--c-teal)",[78,108,112],{"x":109,"y":110,"fill":82,"style":111},"155","212","text-anchor:middle;font-size:12;font-family:var(--font-sans);","$20",[78,114,117],{"x":109,"y":115,"fill":88,"style":116},"290","text-anchor:middle;font-size:11;font-family:var(--font-sans);","10 accts",[71,119],{"x":120,"y":121,"width":103,"height":122,"rx":105,"fill":123},"255","170","100","var(--c-blue)",[78,125,128],{"x":126,"y":127,"fill":82,"style":111},"300","162","$200",[78,130,131],{"x":126,"y":115,"fill":88,"style":116},"100 accts",[71,133],{"x":134,"y":135,"width":103,"height":136,"rx":105,"fill":137},"400","120","150","var(--c-yellow)",[78,139,142],{"x":140,"y":141,"fill":82,"style":111},"445","112","$2,000",[78,144,145],{"x":140,"y":115,"fill":88,"style":116},"1k accts",[71,147],{"x":148,"y":94,"width":103,"height":149,"rx":105,"fill":150},"545","200","var(--c-coral)",[78,152,155],{"x":153,"y":154,"fill":82,"style":111},"590","62","$20,000",[78,157,158],{"x":153,"y":115,"fill":88,"style":116},"10k accts",[14,160,161,162,165],{},"The uncomfortable part is that the last bar arrives without warning. Farming is not a gradual ramp you watch on a dashboard; it is a burst. An attacker who finds your undefended signup form goes from zero to ten thousand accounts in the time it takes their script to solve enough email confirmations, which is why the defenses below have to be in place ",[35,163,164],{},"before"," you are interesting, not bolted on after the invoice lands.",[24,167],{},[27,169,171],{"id":170},"the-defense-layers","The defense layers",[14,173,174],{},"Think of the defenses as a pipeline every signup and request flows through. Each stage is cheap, each one filters a different class of attacker, and a request only reaches your billable code once it has cleared all of them. The stages are ordered by cost-to-you, cheapest first, so the overwhelming majority of junk is rejected before you spend a Redis round-trip on it, let alone an upstream call.",[52,176,61,181,61,184,61,187,61,189,61,207,61,211,61,216,61,221,61,224,61,229,61,233,61,237,61,239,61,243,61,246,61,248,61,250,61,254,61,257,61,260,61,262,61,265,61,268,61,270,61,272,61,276,61,278,61,281,61,285],{"viewBox":177,"role":55,"ariaLabelledBy":178,"xmlns":59,"style":60},"0 0 720 300",[179,180],"fta-flow-t","fta-flow-d",[63,182,183],{"id":179},"Free-tier defense pipeline",[67,185,186],{"id":180},"A signup and request pass through disposable-email blocking, identity verification, per-IP velocity, and quota checks before being allowed or blocked with a 429 or 402.",[71,188],{"x":73,"y":73,"width":74,"height":126,"fill":76},[190,191,192,193,61],"defs",{},"\n    ",[194,195,202,203,192],"marker",{"id":196,"viewBox":197,"refX":198,"refY":199,"markerWidth":200,"markerHeight":200,"orient":201},"fta-arrow","0 0 10 10","9","5","7","auto-start-reverse","\n      ",[204,205],"path",{"d":206,"fill":88},"M0 0 L10 5 L0 10 z",[71,208],{"x":80,"y":80,"width":136,"height":104,"rx":209,"fill":76,"stroke":123,"style":210},"10","stroke-width:2;",[78,212,215],{"x":213,"y":104,"fill":82,"style":214},"95","text-anchor:middle;font-size:13;font-family:var(--font-sans);","Signup \u002F request",[92,217],{"x1":121,"y1":218,"x2":219,"y2":218,"stroke":88,"style":220},"45","230","stroke-width:2;marker-end:url(#fta-arrow);",[71,222],{"x":219,"y":80,"width":223,"height":104,"rx":209,"fill":76,"stroke":106,"style":210},"160",[78,225,228],{"x":226,"y":227,"fill":82,"style":111},"310","42","Disposable email?",[78,230,232],{"x":226,"y":231,"fill":88,"style":116},"60","block list",[92,234],{"x1":235,"y1":218,"x2":236,"y2":218,"stroke":88,"style":220},"390","450",[71,238],{"x":236,"y":80,"width":223,"height":104,"rx":209,"fill":76,"stroke":106,"style":210},[78,240,242],{"x":241,"y":227,"fill":82,"style":111},"530","Verified identity?",[78,244,245],{"x":241,"y":231,"fill":88,"style":116},"email \u002F phone",[92,247],{"x1":241,"y1":94,"x2":241,"y2":135,"stroke":88,"style":220},[71,249],{"x":236,"y":135,"width":223,"height":104,"rx":209,"fill":76,"stroke":106,"style":210},[78,251,253],{"x":241,"y":252,"fill":82,"style":111},"142","Per-IP velocity OK?",[78,255,256],{"x":241,"y":223,"fill":88,"style":116},"sliding window",[92,258],{"x1":236,"y1":259,"x2":235,"y2":259,"stroke":88,"style":220},"145",[71,261],{"x":219,"y":135,"width":223,"height":104,"rx":209,"fill":76,"stroke":106,"style":210},[78,263,264],{"x":226,"y":252,"fill":82,"style":111},"Quota remaining?",[78,266,267],{"x":226,"y":223,"fill":88,"style":116},"monthly counter",[92,269],{"x1":226,"y1":121,"x2":226,"y2":102,"stroke":88,"style":220},[71,271],{"x":219,"y":102,"width":223,"height":104,"rx":209,"fill":76,"stroke":106,"style":210},[78,273,275],{"x":226,"y":274,"fill":106,"style":214},"250","Allow request",[71,277],{"x":236,"y":102,"width":223,"height":104,"rx":209,"fill":76,"stroke":150,"style":210},[78,279,280],{"x":241,"y":274,"fill":150,"style":214},"Block \u002F 429 \u002F 402",[92,282],{"x1":235,"y1":283,"x2":236,"y2":283,"stroke":150,"style":284},"245","stroke-width:2;stroke-dasharray:4 3;marker-end:url(#fta-arrow);",[78,286,290],{"x":287,"y":288,"fill":88,"style":289},"420","237","text-anchor:middle;font-size:10;font-family:var(--font-sans);","any fail",[292,293,295],"h3",{"id":294},"identity-verification","Identity verification",[14,297,298,299,303,304,307,308,312,313,316],{},"Require ",[300,301,302],"strong",{},"email verification"," at minimum — a confirmation link before the key activates kills the cheapest bots. Combine it with ",[300,305,306],{},"disposable-email blocking",": reject signups whose domain is on a known throwaway-provider list, refreshed regularly. A maintained list such as the community ",[309,310,311],"code",{},"disposable-email-domains"," dataset covers tens of thousands of domains and can be loaded into a set at boot for O(1) lookups, but treat it as a moving target — new throwaway providers appear weekly, and a stale list is a list that has already been beaten. For higher-value free tiers, add ",[300,314,315],{},"phone verification"," (an SMS one-time code), which raises the per-account cost far enough to deter most farming, since phone numbers are harder and costlier to acquire in bulk than email addresses. The trade-off is real money and real friction: SMS verification costs you a few cents per send through a provider, and every extra field on the signup form measurably drops conversion, so reserve phone for tiers where the upstream cost of abuse clearly exceeds that friction.",[292,318,320],{"id":319},"card-on-file","Card-on-file",[14,322,323,324,327,328,332,333,337],{},"The strongest signal short of charging money is ",[300,325,326],{},"a verified card with no charge",". Requiring a card to start the free tier filters out essentially all automated abuse while staying free for the honest developer, and it pre-positions you to convert them or bill overages later. You can authorize and immediately void a small amount to confirm the card is real. This is the same payment rail you already stand up when you ",[18,329,331],{"href":330},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fhow-to-charge-for-api-access-using-stripe\u002F","charge for API access using Stripe",", and it pairs naturally with ",[18,334,336],{"href":335},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fstripe-metered-billing-configuration\u002F","Stripe metered billing configuration"," so overage past the free quota bills automatically instead of just erroring. Card-on-file is the heaviest layer on conversion, though: expect it to cut top-of-funnel signups by a third or more, which is why it belongs in reserve rather than on the default path.",[292,339,341],{"id":340},"rate-quota-and-velocity","Rate, quota, and velocity",[14,343,344],{},"Identity caps how many accounts exist; limits cap what each can do. Enforce three counters together:",[346,347,348,355,361],"ul",{},[349,350,351,354],"li",{},[300,352,353],{},"Per-account quota"," — the monthly free allowance (e.g. 1,000 calls).",[349,356,357,360],{},[300,358,359],{},"Per-IP velocity"," — a sliding window that catches one origin spraying many young accounts, independent of which key it presents.",[349,362,363,366],{},[300,364,365],{},"Burst rate"," — a short-window limit that stops a single key from hammering you faster than a human integration would.",[14,368,369,370,374],{},"Velocity checks are what catch account farming specifically: a single IP creating accounts and calling at machine speed trips the IP window long before any individual account hits its quota. These limits build directly on the same primitives covered in ",[18,371,373],{"href":372},"\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fbest-practices-for-api-rate-limiting\u002F","best practices for API rate limiting","; the difference here is that you key one window on network origin rather than on the API key, which is what turns a rate limiter into an anti-farming tool.",[14,376,377],{},"No single layer is complete. The table below ranks each by how much it deters bots, how much friction it adds to honest signups, and what it costs you to run — read down the columns to see why email verification and the quota-plus-velocity guard are the default pair, and phone and card are escalations.",[379,380,381,400],"table",{},[382,383,384],"thead",{},[385,386,387,391,394,397],"tr",{},[388,389,390],"th",{},"Layer",[388,392,393],{},"Bots deterred",[388,395,396],{},"Signup friction",[388,398,399],{},"Your cost",[401,402,403,418,432,444],"tbody",{},[385,404,405,409,412,415],{},[406,407,408],"td",{},"Email verify + block list",[406,410,411],{},"Medium",[406,413,414],{},"Low",[406,416,417],{},"Near zero",[385,419,420,423,426,429],{},[406,421,422],{},"Quota + IP velocity",[406,424,425],{},"High",[406,427,428],{},"None",[406,430,431],{},"Redis calls",[385,433,434,437,439,441],{},[406,435,436],{},"Phone (SMS OTP)",[406,438,425],{},[406,440,411],{},[406,442,443],{},"Cents\u002Fsend",[385,445,446,448,451,453],{},[406,447,320],{},[406,449,450],{},"Very high",[406,452,425],{},[406,454,455],{},"Stripe fees",[52,457,61,461,61,464,61,467,61,469,61,472,61,475,61,478,61,483,61,489,61,491,61,493,61,495,61,498,61,500,61,504,61,506,61,508,61,510,61,512,61,514,61,516,61,519,61,522,61,524,61,526,61,528,61,530,61,532,61,535,61,538,61,540,61,542,61,544,61,546,61,548],{"viewBox":177,"role":55,"ariaLabelledBy":458,"xmlns":59,"style":60},[459,460],"fta-matrix-t","fta-matrix-d",[63,462,463],{"id":459},"Defense layers compared on deterrence, friction, and cost",[67,465,466],{"id":460},"A matrix rating email verification, quota plus velocity, phone verification, and card-on-file across how many bots each deters, how much signup friction it adds, and what it costs to run.",[71,468],{"x":73,"y":73,"width":74,"height":126,"fill":76},[78,470,393],{"x":102,"y":471,"fill":82,"style":111},"34",[78,473,396],{"x":474,"y":471,"fill":82,"style":111},"380",[78,476,399],{"x":477,"y":471,"fill":82,"style":111},"560",[78,479,482],{"x":80,"y":480,"fill":82,"style":481},"88","font-size:12;font-family:var(--font-sans);","Email + block list",[71,484],{"x":136,"y":485,"width":486,"height":471,"rx":487,"fill":137,"fillOpacity":488},"66","140","4",0.24,[78,490,411],{"x":102,"y":480,"fill":82,"style":111},[71,492],{"x":226,"y":485,"width":486,"height":471,"rx":487,"fill":106,"fillOpacity":488},[78,494,414],{"x":474,"y":480,"fill":82,"style":111},[71,496],{"x":497,"y":485,"width":486,"height":471,"rx":487,"fill":106,"fillOpacity":488},"490",[78,499,417],{"x":477,"y":480,"fill":82,"style":111},[78,501,503],{"x":80,"y":502,"fill":82,"style":481},"134","Quota + velocity",[71,505],{"x":136,"y":141,"width":486,"height":471,"rx":487,"fill":106,"fillOpacity":488},[78,507,425],{"x":102,"y":502,"fill":82,"style":111},[71,509],{"x":226,"y":141,"width":486,"height":471,"rx":487,"fill":106,"fillOpacity":488},[78,511,428],{"x":474,"y":502,"fill":82,"style":111},[71,513],{"x":497,"y":141,"width":486,"height":471,"rx":487,"fill":106,"fillOpacity":488},[78,515,431],{"x":477,"y":502,"fill":82,"style":111},[78,517,436],{"x":80,"y":518,"fill":82,"style":481},"180",[71,520],{"x":136,"y":521,"width":486,"height":471,"rx":487,"fill":106,"fillOpacity":488},"158",[78,523,425],{"x":102,"y":518,"fill":82,"style":111},[71,525],{"x":226,"y":521,"width":486,"height":471,"rx":487,"fill":137,"fillOpacity":488},[78,527,411],{"x":474,"y":518,"fill":82,"style":111},[71,529],{"x":497,"y":521,"width":486,"height":471,"rx":487,"fill":137,"fillOpacity":488},[78,531,443],{"x":477,"y":518,"fill":82,"style":111},[78,533,320],{"x":80,"y":534,"fill":82,"style":481},"226",[71,536],{"x":136,"y":537,"width":486,"height":471,"rx":487,"fill":106,"fillOpacity":488},"204",[78,539,450],{"x":102,"y":534,"fill":82,"style":111},[71,541],{"x":226,"y":537,"width":486,"height":471,"rx":487,"fill":150,"fillOpacity":488},[78,543,425],{"x":474,"y":534,"fill":82,"style":111},[71,545],{"x":497,"y":537,"width":486,"height":471,"rx":487,"fill":137,"fillOpacity":488},[78,547,455],{"x":477,"y":534,"fill":82,"style":111},[78,549,551],{"x":80,"y":550,"fill":88,"style":89},"272","teal = favorable, yellow = moderate, coral = costly — ship the top two, hold the bottom two in reserve",[24,553],{},[27,555,557],{"id":556},"runnable-fastapi-guard","Runnable FastAPI guard",[14,559,560,561,564,565,568],{},"This dependency enforces a free-tier monthly quota and a per-IP velocity window in Redis, atomically. It fails closed on quota (you do not give away calls when the counter is unreadable) and returns the correct status codes: ",[309,562,563],{},"429"," for velocity, ",[309,566,567],{},"402"," when the free quota is exhausted and an upgrade is required. Configuration comes from the environment.",[570,571,576],"pre",{"className":572,"code":573,"language":574,"meta":575,"style":575},"language-python shiki shiki-themes github-light github-dark","import os\nimport time\nimport redis.asyncio as redis\nfrom fastapi import FastAPI, Request, Header, HTTPException, Depends\n\napp = FastAPI()\nr = redis.Redis.from_url(os.getenv(\"REDIS_URL\", \"redis:\u002F\u002Flocalhost:6379\u002F0\"), decode_responses=True)\n\nFREE_QUOTA = int(os.getenv(\"FREE_TIER_QUOTA\", \"1000\"))          # calls per month\nIP_WINDOW = int(os.getenv(\"IP_VELOCITY_WINDOW\", \"60\"))          # seconds\nIP_MAX = int(os.getenv(\"IP_VELOCITY_MAX\", \"30\"))                # calls per window per IP\n\n# Atomic sliding-window velocity check for one key.\n_VELOCITY = \"\"\"\nredis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1] - ARGV[2])\nlocal n = redis.call('ZCARD', KEYS[1])\nif n >= tonumber(ARGV[3]) then return 0 end\nredis.call('ZADD', KEYS[1], ARGV[1], ARGV[1] .. ':' .. ARGV[4])\nredis.call('EXPIRE', KEYS[1], ARGV[2])\nreturn 1\n\"\"\"\n\nasync def free_tier_guard(request: Request, x_api_key: str = Header(...)) -> dict:\n    now = time.time()\n    ip = request.headers.get(\"X-Forwarded-For\", request.client.host).split(\",\")[0].strip()\n\n    # 1. Per-IP velocity — catches account farming from one origin.\n    vscript = r.register_script(_VELOCITY)\n    ok = await vscript(keys=[f\"vel:ip:{ip}\"], args=[now, IP_WINDOW, IP_MAX, os.urandom(4).hex()])\n    if not ok:\n        raise HTTPException(status_code=429, detail=\"Too many requests from this network.\",\n                            headers={\"Retry-After\": str(IP_WINDOW)})\n\n    # 2. Per-account monthly quota — fails closed.\n    period = time.strftime(\"%Y-%m\")\n    used = await r.incr(f\"quota:{x_api_key}:{period}\")\n    if used == 1:\n        await r.expire(f\"quota:{x_api_key}:{period}\", int(os.getenv(\"QUOTA_TTL\", \"2764800\")))\n    if used > FREE_QUOTA:\n        raise HTTPException(status_code=402, detail=\"Free quota exhausted. Upgrade to continue.\")\n\n    return {\"key\": x_api_key, \"remaining\": FREE_QUOTA - used}\n\n\n@app.get(\"\u002Fv1\u002Fdata\")\nasync def get_data(guard: dict = Depends(free_tier_guard)):\n    return {\"data\": \"ok\", \"quota_remaining\": guard[\"remaining\"]}\n","python","",[309,577,578,590,598,612,626,633,645,682,687,717,742,768,773,779,790,796,802,808,814,820,826,832,837,873,884,912,917,923,938,1003,1015,1044,1070,1075,1081,1097,1136,1152,1199,1214,1238,1243,1271,1276,1281,1294,1314],{"__ignoreMap":575},[579,580,582,586],"span",{"class":92,"line":581},1,[579,583,585],{"class":584},"szBVR","import",[579,587,589],{"class":588},"sVt8B"," os\n",[579,591,593,595],{"class":92,"line":592},2,[579,594,585],{"class":584},[579,596,597],{"class":588}," time\n",[579,599,601,603,606,609],{"class":92,"line":600},3,[579,602,585],{"class":584},[579,604,605],{"class":588}," redis.asyncio ",[579,607,608],{"class":584},"as",[579,610,611],{"class":588}," redis\n",[579,613,615,618,621,623],{"class":92,"line":614},4,[579,616,617],{"class":584},"from",[579,619,620],{"class":588}," fastapi ",[579,622,585],{"class":584},[579,624,625],{"class":588}," FastAPI, Request, Header, HTTPException, Depends\n",[579,627,629],{"class":92,"line":628},5,[579,630,632],{"emptyLinePlaceholder":631},true,"\n",[579,634,636,639,642],{"class":92,"line":635},6,[579,637,638],{"class":588},"app ",[579,640,641],{"class":584},"=",[579,643,644],{"class":588}," FastAPI()\n",[579,646,648,651,653,656,660,663,666,669,673,675,679],{"class":92,"line":647},7,[579,649,650],{"class":588},"r ",[579,652,641],{"class":584},[579,654,655],{"class":588}," redis.Redis.from_url(os.getenv(",[579,657,659],{"class":658},"sZZnC","\"REDIS_URL\"",[579,661,662],{"class":588},", ",[579,664,665],{"class":658},"\"redis:\u002F\u002Flocalhost:6379\u002F0\"",[579,667,668],{"class":588},"), ",[579,670,672],{"class":671},"s4XuR","decode_responses",[579,674,641],{"class":584},[579,676,678],{"class":677},"sj4cs","True",[579,680,681],{"class":588},")\n",[579,683,685],{"class":92,"line":684},8,[579,686,632],{"emptyLinePlaceholder":631},[579,688,690,693,696,699,702,705,707,710,713],{"class":92,"line":689},9,[579,691,692],{"class":677},"FREE_QUOTA",[579,694,695],{"class":584}," =",[579,697,698],{"class":677}," int",[579,700,701],{"class":588},"(os.getenv(",[579,703,704],{"class":658},"\"FREE_TIER_QUOTA\"",[579,706,662],{"class":588},[579,708,709],{"class":658},"\"1000\"",[579,711,712],{"class":588},"))          ",[579,714,716],{"class":715},"sJ8bj","# calls per month\n",[579,718,720,723,725,727,729,732,734,737,739],{"class":92,"line":719},10,[579,721,722],{"class":677},"IP_WINDOW",[579,724,695],{"class":584},[579,726,698],{"class":677},[579,728,701],{"class":588},[579,730,731],{"class":658},"\"IP_VELOCITY_WINDOW\"",[579,733,662],{"class":588},[579,735,736],{"class":658},"\"60\"",[579,738,712],{"class":588},[579,740,741],{"class":715},"# seconds\n",[579,743,745,748,750,752,754,757,759,762,765],{"class":92,"line":744},11,[579,746,747],{"class":677},"IP_MAX",[579,749,695],{"class":584},[579,751,698],{"class":677},[579,753,701],{"class":588},[579,755,756],{"class":658},"\"IP_VELOCITY_MAX\"",[579,758,662],{"class":588},[579,760,761],{"class":658},"\"30\"",[579,763,764],{"class":588},"))                ",[579,766,767],{"class":715},"# calls per window per IP\n",[579,769,771],{"class":92,"line":770},12,[579,772,632],{"emptyLinePlaceholder":631},[579,774,776],{"class":92,"line":775},13,[579,777,778],{"class":715},"# Atomic sliding-window velocity check for one key.\n",[579,780,782,785,787],{"class":92,"line":781},14,[579,783,784],{"class":677},"_VELOCITY",[579,786,695],{"class":584},[579,788,789],{"class":658}," \"\"\"\n",[579,791,793],{"class":92,"line":792},15,[579,794,795],{"class":658},"redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1] - ARGV[2])\n",[579,797,799],{"class":92,"line":798},16,[579,800,801],{"class":658},"local n = redis.call('ZCARD', KEYS[1])\n",[579,803,805],{"class":92,"line":804},17,[579,806,807],{"class":658},"if n >= tonumber(ARGV[3]) then return 0 end\n",[579,809,811],{"class":92,"line":810},18,[579,812,813],{"class":658},"redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1] .. ':' .. ARGV[4])\n",[579,815,817],{"class":92,"line":816},19,[579,818,819],{"class":658},"redis.call('EXPIRE', KEYS[1], ARGV[2])\n",[579,821,823],{"class":92,"line":822},20,[579,824,825],{"class":658},"return 1\n",[579,827,829],{"class":92,"line":828},21,[579,830,831],{"class":658},"\"\"\"\n",[579,833,835],{"class":92,"line":834},22,[579,836,632],{"emptyLinePlaceholder":631},[579,838,840,843,846,850,853,856,858,861,864,867,870],{"class":92,"line":839},23,[579,841,842],{"class":584},"async",[579,844,845],{"class":584}," def",[579,847,849],{"class":848},"sScJk"," free_tier_guard",[579,851,852],{"class":588},"(request: Request, x_api_key: ",[579,854,855],{"class":677},"str",[579,857,695],{"class":584},[579,859,860],{"class":588}," Header(",[579,862,863],{"class":677},"...",[579,865,866],{"class":588},")) -> ",[579,868,869],{"class":677},"dict",[579,871,872],{"class":588},":\n",[579,874,876,879,881],{"class":92,"line":875},24,[579,877,878],{"class":588},"    now ",[579,880,641],{"class":584},[579,882,883],{"class":588}," time.time()\n",[579,885,887,890,892,895,898,901,904,907,909],{"class":92,"line":886},25,[579,888,889],{"class":588},"    ip ",[579,891,641],{"class":584},[579,893,894],{"class":588}," request.headers.get(",[579,896,897],{"class":658},"\"X-Forwarded-For\"",[579,899,900],{"class":588},", request.client.host).split(",[579,902,903],{"class":658},"\",\"",[579,905,906],{"class":588},")[",[579,908,73],{"class":677},[579,910,911],{"class":588},"].strip()\n",[579,913,915],{"class":92,"line":914},26,[579,916,632],{"emptyLinePlaceholder":631},[579,918,920],{"class":92,"line":919},27,[579,921,922],{"class":715},"    # 1. Per-IP velocity — catches account farming from one origin.\n",[579,924,926,929,931,934,936],{"class":92,"line":925},28,[579,927,928],{"class":588},"    vscript ",[579,930,641],{"class":584},[579,932,933],{"class":588}," r.register_script(",[579,935,784],{"class":677},[579,937,681],{"class":588},[579,939,941,944,946,949,952,955,957,960,963,966,969,972,975,978,981,984,986,989,991,993,995,998,1000],{"class":92,"line":940},29,[579,942,943],{"class":588},"    ok ",[579,945,641],{"class":584},[579,947,948],{"class":584}," await",[579,950,951],{"class":588}," vscript(",[579,953,954],{"class":671},"keys",[579,956,641],{"class":584},[579,958,959],{"class":588},"[",[579,961,962],{"class":584},"f",[579,964,965],{"class":658},"\"vel:ip:",[579,967,968],{"class":677},"{",[579,970,971],{"class":588},"ip",[579,973,974],{"class":677},"}",[579,976,977],{"class":658},"\"",[579,979,980],{"class":588},"], ",[579,982,983],{"class":671},"args",[579,985,641],{"class":584},[579,987,988],{"class":588},"[now, ",[579,990,722],{"class":677},[579,992,662],{"class":588},[579,994,747],{"class":677},[579,996,997],{"class":588},", os.urandom(",[579,999,487],{"class":677},[579,1001,1002],{"class":588},").hex()])\n",[579,1004,1006,1009,1012],{"class":92,"line":1005},30,[579,1007,1008],{"class":584},"    if",[579,1010,1011],{"class":584}," not",[579,1013,1014],{"class":588}," ok:\n",[579,1016,1018,1021,1024,1027,1029,1031,1033,1036,1038,1041],{"class":92,"line":1017},31,[579,1019,1020],{"class":584},"        raise",[579,1022,1023],{"class":588}," HTTPException(",[579,1025,1026],{"class":671},"status_code",[579,1028,641],{"class":584},[579,1030,563],{"class":677},[579,1032,662],{"class":588},[579,1034,1035],{"class":671},"detail",[579,1037,641],{"class":584},[579,1039,1040],{"class":658},"\"Too many requests from this network.\"",[579,1042,1043],{"class":588},",\n",[579,1045,1047,1050,1052,1054,1057,1060,1062,1065,1067],{"class":92,"line":1046},32,[579,1048,1049],{"class":671},"                            headers",[579,1051,641],{"class":584},[579,1053,968],{"class":588},[579,1055,1056],{"class":658},"\"Retry-After\"",[579,1058,1059],{"class":588},": ",[579,1061,855],{"class":677},[579,1063,1064],{"class":588},"(",[579,1066,722],{"class":677},[579,1068,1069],{"class":588},")})\n",[579,1071,1073],{"class":92,"line":1072},33,[579,1074,632],{"emptyLinePlaceholder":631},[579,1076,1078],{"class":92,"line":1077},34,[579,1079,1080],{"class":715},"    # 2. Per-account monthly quota — fails closed.\n",[579,1082,1084,1087,1089,1092,1095],{"class":92,"line":1083},35,[579,1085,1086],{"class":588},"    period ",[579,1088,641],{"class":584},[579,1090,1091],{"class":588}," time.strftime(",[579,1093,1094],{"class":658},"\"%Y-%m\"",[579,1096,681],{"class":588},[579,1098,1100,1103,1105,1107,1110,1112,1115,1117,1120,1122,1125,1127,1130,1132,1134],{"class":92,"line":1099},36,[579,1101,1102],{"class":588},"    used ",[579,1104,641],{"class":584},[579,1106,948],{"class":584},[579,1108,1109],{"class":588}," r.incr(",[579,1111,962],{"class":584},[579,1113,1114],{"class":658},"\"quota:",[579,1116,968],{"class":677},[579,1118,1119],{"class":588},"x_api_key",[579,1121,974],{"class":677},[579,1123,1124],{"class":658},":",[579,1126,968],{"class":677},[579,1128,1129],{"class":588},"period",[579,1131,974],{"class":677},[579,1133,977],{"class":658},[579,1135,681],{"class":588},[579,1137,1139,1141,1144,1147,1150],{"class":92,"line":1138},37,[579,1140,1008],{"class":584},[579,1142,1143],{"class":588}," used ",[579,1145,1146],{"class":584},"==",[579,1148,1149],{"class":677}," 1",[579,1151,872],{"class":588},[579,1153,1155,1158,1161,1163,1165,1167,1169,1171,1173,1175,1177,1179,1181,1183,1186,1188,1191,1193,1196],{"class":92,"line":1154},38,[579,1156,1157],{"class":584},"        await",[579,1159,1160],{"class":588}," r.expire(",[579,1162,962],{"class":584},[579,1164,1114],{"class":658},[579,1166,968],{"class":677},[579,1168,1119],{"class":588},[579,1170,974],{"class":677},[579,1172,1124],{"class":658},[579,1174,968],{"class":677},[579,1176,1129],{"class":588},[579,1178,974],{"class":677},[579,1180,977],{"class":658},[579,1182,662],{"class":588},[579,1184,1185],{"class":677},"int",[579,1187,701],{"class":588},[579,1189,1190],{"class":658},"\"QUOTA_TTL\"",[579,1192,662],{"class":588},[579,1194,1195],{"class":658},"\"2764800\"",[579,1197,1198],{"class":588},")))\n",[579,1200,1202,1204,1206,1209,1212],{"class":92,"line":1201},39,[579,1203,1008],{"class":584},[579,1205,1143],{"class":588},[579,1207,1208],{"class":584},">",[579,1210,1211],{"class":677}," FREE_QUOTA",[579,1213,872],{"class":588},[579,1215,1217,1219,1221,1223,1225,1227,1229,1231,1233,1236],{"class":92,"line":1216},40,[579,1218,1020],{"class":584},[579,1220,1023],{"class":588},[579,1222,1026],{"class":671},[579,1224,641],{"class":584},[579,1226,567],{"class":677},[579,1228,662],{"class":588},[579,1230,1035],{"class":671},[579,1232,641],{"class":584},[579,1234,1235],{"class":658},"\"Free quota exhausted. Upgrade to continue.\"",[579,1237,681],{"class":588},[579,1239,1241],{"class":92,"line":1240},41,[579,1242,632],{"emptyLinePlaceholder":631},[579,1244,1246,1249,1252,1255,1258,1261,1263,1265,1268],{"class":92,"line":1245},42,[579,1247,1248],{"class":584},"    return",[579,1250,1251],{"class":588}," {",[579,1253,1254],{"class":658},"\"key\"",[579,1256,1257],{"class":588},": x_api_key, ",[579,1259,1260],{"class":658},"\"remaining\"",[579,1262,1059],{"class":588},[579,1264,692],{"class":677},[579,1266,1267],{"class":584}," -",[579,1269,1270],{"class":588}," used}\n",[579,1272,1274],{"class":92,"line":1273},43,[579,1275,632],{"emptyLinePlaceholder":631},[579,1277,1279],{"class":92,"line":1278},44,[579,1280,632],{"emptyLinePlaceholder":631},[579,1282,1284,1287,1289,1292],{"class":92,"line":1283},45,[579,1285,1286],{"class":848},"@app.get",[579,1288,1064],{"class":588},[579,1290,1291],{"class":658},"\"\u002Fv1\u002Fdata\"",[579,1293,681],{"class":588},[579,1295,1297,1299,1301,1304,1307,1309,1311],{"class":92,"line":1296},46,[579,1298,842],{"class":584},[579,1300,845],{"class":584},[579,1302,1303],{"class":848}," get_data",[579,1305,1306],{"class":588},"(guard: ",[579,1308,869],{"class":677},[579,1310,695],{"class":584},[579,1312,1313],{"class":588}," Depends(free_tier_guard)):\n",[579,1315,1317,1319,1321,1324,1326,1329,1331,1334,1337,1339],{"class":92,"line":1316},47,[579,1318,1248],{"class":584},[579,1320,1251],{"class":588},[579,1322,1323],{"class":658},"\"data\"",[579,1325,1059],{"class":588},[579,1327,1328],{"class":658},"\"ok\"",[579,1330,662],{"class":588},[579,1332,1333],{"class":658},"\"quota_remaining\"",[579,1335,1336],{"class":588},": guard[",[579,1338,1260],{"class":658},[579,1340,1341],{"class":588},"]}\n",[14,1343,1344,1345,1347,1348,1350],{},"The velocity check runs first and is keyed on IP, not on the API key, so it sees a farm of fresh accounts as one offender. The quota counter is keyed per account and per month, with a TTL so old periods clean themselves up. Returning ",[309,1346,567],{}," rather than ",[309,1349,563],{}," on quota exhaustion is deliberate: it tells an honest developer to upgrade, and your dashboard can surface that as a conversion prompt. The sequence below traces one request through both counters and shows where each response code branches off.",[52,1352,61,1357,61,1360,61,1363,61,1366,61,1373,61,1378,61,1382,61,1384,61,1388,61,1390,61,1394,61,1398,61,1400,61,1402,61,1406,61,1410,61,1412,61,1416,61,1419,61,1423,61,1426,61,1430,61,1432,61,1435,61,1437,61,1441],{"viewBox":1353,"role":55,"ariaLabelledBy":1354,"xmlns":59,"style":60},"0 0 720 330",[1355,1356],"fta-seq-t","fta-seq-d",[63,1358,1359],{"id":1355},"Request sequence through the velocity and quota guard",[67,1361,1362],{"id":1356},"A client call reaches FastAPI, which runs the atomic IP velocity script and the monthly quota counter in Redis, returning 429, 402, or 200 depending on which check fails.",[71,1364],{"x":73,"y":73,"width":74,"height":1365,"fill":76},"330",[190,1367,192,1368,61],{},[194,1369,202,1371,192],{"id":1370,"viewBox":197,"refX":198,"refY":199,"markerWidth":200,"markerHeight":200,"orient":201},"fta-seq-arrow",[204,1372],{"d":206,"fill":88},[71,1374],{"x":1375,"y":80,"width":135,"height":1376,"rx":1377,"fill":76,"stroke":123,"style":210},"40","36","6",[78,1379,1381],{"x":122,"y":1380,"fill":82,"style":111},"43","Client",[71,1383],{"x":126,"y":80,"width":135,"height":1376,"rx":1377,"fill":76,"stroke":106,"style":210},[78,1385,1387],{"x":1386,"y":1380,"fill":82,"style":111},"360","FastAPI guard",[71,1389],{"x":477,"y":80,"width":135,"height":1376,"rx":1377,"fill":76,"stroke":150,"style":210},[78,1391,1393],{"x":1392,"y":1380,"fill":82,"style":111},"620","Redis",[92,1395],{"x1":122,"y1":1396,"x2":122,"y2":226,"stroke":97,"style":1397},"56","stroke-width:1;stroke-dasharray:3 3;",[92,1399],{"x1":1386,"y1":1396,"x2":1386,"y2":226,"stroke":97,"style":1397},[92,1401],{"x1":1392,"y1":1396,"x2":1392,"y2":226,"stroke":97,"style":1397},[92,1403],{"x1":122,"y1":1404,"x2":1386,"y2":1404,"stroke":88,"style":1405},"82","stroke-width:2;marker-end:url(#fta-seq-arrow);",[78,1407,1409],{"x":219,"y":1408,"fill":82,"style":116},"76","GET \u002Fv1\u002Fdata + X-API-Key",[92,1411],{"x1":1386,"y1":141,"x2":1392,"y2":141,"stroke":88,"style":1405},[78,1413,1415],{"x":497,"y":1414,"fill":82,"style":116},"106","ZADD vel:ip window",[92,1417],{"x1":1386,"y1":136,"x2":122,"y2":136,"stroke":150,"style":1418},"stroke-width:2;stroke-dasharray:4 3;marker-end:url(#fta-seq-arrow);",[78,1420,1422],{"x":219,"y":1421,"fill":150,"style":116},"144","429 if window full",[92,1424],{"x1":1386,"y1":1425,"x2":1392,"y2":1425,"stroke":88,"style":1405},"188",[78,1427,1429],{"x":497,"y":1428,"fill":82,"style":116},"182","INCR quota:key:month",[92,1431],{"x1":1386,"y1":534,"x2":122,"y2":534,"stroke":150,"style":1418},[78,1433,1434],{"x":219,"y":102,"fill":150,"style":116},"402 if over quota",[92,1436],{"x1":1386,"y1":550,"x2":122,"y2":550,"stroke":106,"style":1405},[78,1438,1440],{"x":219,"y":1439,"fill":106,"style":116},"266","200 + remaining",[78,1442,1443],{"x":1386,"y":126,"fill":88,"style":116},"two Redis round-trips, both atomic, sub-millisecond on a colocated instance",[14,1445,1446,1447,1450,1451,1454,1455,1457],{},"One production caveat on the IP layer: ",[309,1448,1449],{},"X-Forwarded-For"," is trivially spoofable unless your edge sets it, so read the client IP from the header your platform actually controls — ",[309,1452,1453],{},"CF-Connecting-IP"," behind Cloudflare, or the rightmost trusted hop your load balancer appends — never the raw leftmost value a client can forge. Get this wrong and an attacker rotates a fake ",[309,1456,1449],{}," per request and your velocity window never fills. The velocity script itself is safe to run on every request because it is a single round-trip and Redis evaluates it atomically; if you colocate Redis with the API it adds well under a millisecond, and even a managed instance a few milliseconds away is cheap next to the upstream call it protects.",[24,1459],{},[27,1461,1463],{"id":1462},"detecting-abuse-before-it-bills-you","Detecting abuse before it bills you",[14,1465,1466,1467,1470,1471,1475,1476,1480,1481,1485],{},"Blocking is only half the job — you also need to ",[35,1468,1469],{},"see"," farming as it starts, because the layer you tune next depends on the pattern you are actually facing. The signal that matters is not raw traffic; it is the ratio of new accounts to unique payment identities and IP ranges. A healthy free tier shows many IPs, many email domains, and steady per-account usage. A farm shows the opposite: a narrow band of IPs or a single ASN, a spike in signups from one or two domains, and a cohort of accounts that each stop dead at exactly your quota ceiling. Emit a usage event per request into Postgres, as covered in ",[18,1472,1474],{"href":1473},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Ftracking-api-usage-and-analytics\u002Flogging-api-usage-events-to-postgres\u002F","logging API usage events to Postgres",", and you can query these ratios directly; surface them on a ",[18,1477,1479],{"href":1478},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Ftracking-api-usage-and-analytics\u002Fbuilding-a-customer-usage-dashboard\u002F","customer usage dashboard"," so the farm cohort stands out as a band of identical-shaped accounts. Wire the same counters into the broader picture from ",[18,1482,1484],{"href":1483},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Ftracking-api-usage-and-analytics\u002F","tracking API usage and analytics"," and you turn abuse detection into the same telemetry that drives your conversion metrics.",[14,1487,1488,1489,1492],{},"Set one alert to start: signups per hour from a single ",[309,1490,1491],{},"\u002F24"," IP block crossing a threshold. It is cheap, it fires on exactly the burst that denial-of-wallet farming produces, and it buys you the hours you need to escalate a layer before the invoice does.",[24,1494],{},[27,1496,1498],{"id":1497},"when-each-layer-earns-its-place","When each layer earns its place",[14,1500,1501,1502,1505,1506,1509],{},"Start with ",[300,1503,1504],{},"email verification plus disposable-email blocking and the Redis quota\u002Fvelocity guard"," — that combination stops the overwhelming majority of abuse at near-zero friction. Add ",[300,1507,1508],{},"phone or card-on-file"," only when you see organized farming or your per-call upstream cost is high enough that even slow abuse hurts. Avoid front-loading all of it on day one: every extra verification step costs you real signups, and over-defending a tier nobody is abusing yet is wasted conversion. Watch the metrics from the section above, and add the next layer when the abuse pattern actually appears. If your endpoints proxy an expensive model, treat card-on-file as an earlier escalation than you otherwise would — the break-even where friction is cheaper than abuse arrives far sooner when a single call costs cents rather than micro-dollars.",[14,1511,1512,1513,1517],{},"There is also a cheaper mitigation people forget: cache. If a meaningful share of free-tier calls are identical reads, a response cache in front of the endpoint — see ",[18,1514,1516],{"href":1515},"\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002F","caching Python API responses with Redis"," — turns repeated abusive calls into cache hits that never reach your upstream, blunting denial of wallet without adding a single signup barrier. It does not stop farming, but it caps the damage each farmed account can do.",[24,1519],{},[27,1521,1523],{"id":1522},"builder-verdict","Builder verdict",[14,1525,1526],{},"Treat the free tier as something you defend in depth, not with a single magic check. Ship email verification, a disposable-domain block list, and the per-account quota plus per-IP velocity guard above on launch day — it is an afternoon of work and it neutralizes scripted account farming. Instrument the signup-to-identity ratio at the same time so a real farm shows up as a pattern you can see, not a bill you discover. Hold phone verification and card-on-file in reserve as escalations for when abuse actually shows up or your upstream costs make slow abuse expensive. The mistake is not under-defending; it is reaching for card-on-file before you have even shipped a velocity check, and strangling acquisition to solve a problem you do not yet have.",[24,1528],{},[27,1530,1532],{"id":1531},"faq","FAQ",[14,1534,1535,1538],{},[300,1536,1537],{},"Should I require a credit card for my free tier?","\nOnly if abuse or high per-call upstream costs justify the friction. A card-on-file requirement stops nearly all automated abuse, but it also cuts signups by a third or more. Start with email verification plus quota and velocity limits, and add a card requirement only when you observe organized farming or your per-call vendor cost makes even slow abuse expensive.",[14,1540,1541,1544],{},[300,1542,1543],{},"How do per-IP limits stop account farming when each account stays under quota?","\nBecause the IP velocity window is keyed on network origin, not the API key. A bot creating dozens of fresh accounts from one IP trips the per-IP sliding window long before any single account reaches its monthly quota, so you block the farm as one offender rather than chasing each account individually.",[14,1546,1547,1550],{},[300,1548,1549],{},"How do I block disposable email addresses?","\nMaintain a block list of known throwaway-provider domains and reject signups matching it during email validation, refreshing the list regularly since new providers appear constantly. Load the list into a set at boot for O(1) lookups, and pair it with a verification link so addresses that pass the domain check still have to prove they receive mail.",[14,1552,1553,1556,1557,1560,1561,1564,1565,1567,1568,1570,1571,1573],{},[300,1554,1555],{},"What status code should I return when the free quota runs out?","\nReturn ",[309,1558,1559],{},"402 Payment Required"," for an exhausted quota and ",[309,1562,1563],{},"429 Too Many Requests"," for a tripped rate or velocity limit. The distinction matters commercially: ",[309,1566,567],{}," signals an honest developer to upgrade and can drive conversion, while ",[309,1569,563],{}," signals transient backoff. Wire the ",[309,1572,567],{}," path to your metered-billing upgrade flow so exhaustion converts instead of just failing.",[14,1575,1576,1579,1580,1583],{},[300,1577,1578],{},"How much does running this guard cost at a million free calls a month?","\nEffectively nothing on the Redis side — two atomic round-trips per request on a small managed instance is a few dollars a month of compute. The cost you are actually managing is upstream: the guard exists so that a million ",[35,1581,1582],{},"abusive"," free calls never reach a vendor that would bill you for them, which at $0.002 a call is the difference between a rounding error and a $2,000 invoice.",[27,1585,1587],{"id":1586},"related","Related",[14,1589,1590],{},[300,1591,1592],{},"Same track:",[346,1594,1595,1600,1606,1612],{},[349,1596,1597,1599],{},[18,1598,21],{"href":20}," — the guide this page sits under, for setting the tiers you are defending.",[349,1601,1602,1605],{},[18,1603,1604],{"href":330},"How to Charge for API Access Using Stripe"," — stand up the card rail card-on-file reuses.",[349,1607,1608,1611],{},[18,1609,1610],{"href":335},"Stripe Metered Billing Configuration"," — bill overage past the free quota automatically.",[349,1613,1614,1618],{},[18,1615,1617],{"href":1616},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","Calculating Cost per API Request"," — the unit economics that tell you how much abuse actually hurts.",[14,1620,1621],{},[300,1622,1623],{},"Other tracks:",[346,1625,1626,1632],{},[349,1627,1628,1631],{},[18,1629,1630],{"href":372},"Best Practices for API Rate Limiting"," — the sliding-window primitives the velocity guard builds on.",[349,1633,1634,1637],{},[18,1635,1636],{"href":1473},"Logging API Usage Events to Postgres"," — capture the signals that expose a farm early.",[1639,1640,1641],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":575,"searchDepth":592,"depth":592,"links":1643},[1644,1645,1650,1651,1652,1653,1654,1655],{"id":29,"depth":592,"text":30},{"id":170,"depth":592,"text":171,"children":1646},[1647,1648,1649],{"id":294,"depth":600,"text":295},{"id":319,"depth":600,"text":320},{"id":340,"depth":600,"text":341},{"id":556,"depth":592,"text":557},{"id":1462,"depth":592,"text":1463},{"id":1497,"depth":592,"text":1498},{"id":1522,"depth":592,"text":1523},{"id":1531,"depth":592,"text":1532},{"id":1586,"depth":592,"text":1587},"Stop free-tier API abuse: email and phone verification, per-account and per-IP limits, card-on-file, quota and velocity checks, with a runnable FastAPI guard.","md",{"pageTitle":1659,"type":1660,"datePublished":1661,"dateModified":1662},"Preventing Free-Tier API Abuse in Python: A Playbook","article","2026-06-18","2026-07-23","\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fpreventing-free-tier-abuse",{"title":5,"description":1656},"building-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fpreventing-free-tier-abuse\u002Findex","R2AeyWm9t8L18RFiPdL5bTNrrV6lIVIjIlHO-TVGajI",{"@context":1668,"@type":1669,"mainEntity":1670},"https:\u002F\u002Fschema.org","FAQPage",[1671,1676,1679,1682,1685],{"@type":1672,"name":1537,"acceptedAnswer":1673},"Question",{"@type":1674,"text":1675},"Answer","Only if abuse or high per-call upstream costs justify the friction. A card-on-file requirement stops nearly all automated abuse, but it also cuts signups by a third or more. Start with email verification plus quota and velocity limits, and add a card requirement only when you observe organized farming or your per-call vendor cost makes even slow abuse expensive.",{"@type":1672,"name":1543,"acceptedAnswer":1677},{"@type":1674,"text":1678},"Because the IP velocity window is keyed on network origin, not the API key. A bot creating dozens of fresh accounts from one IP trips the per-IP sliding window long before any single account reaches its monthly quota, so you block the farm as one offender rather than chasing each account individually.",{"@type":1672,"name":1549,"acceptedAnswer":1680},{"@type":1674,"text":1681},"Maintain a block list of known throwaway-provider domains and reject signups matching it during email validation, refreshing the list regularly since new providers appear constantly. Load the list into a set at boot for O(1) lookups, and pair it with a verification link so addresses that pass the domain check still have to prove they receive mail.",{"@type":1672,"name":1555,"acceptedAnswer":1683},{"@type":1674,"text":1684},"Return 402 Payment Required for an exhausted quota and 429 Too Many Requests for a tripped rate or velocity limit. The distinction matters commercially: 402 signals an honest developer to upgrade and can drive conversion, while 429 signals transient backoff. Wire the 402 path to your metered-billing upgrade flow so exhaustion converts instead of just failing.",{"@type":1672,"name":1578,"acceptedAnswer":1686},{"@type":1674,"text":1687},"Effectively nothing on the Redis side — two atomic round-trips per request on a small managed instance is a few dollars a month of compute. The cost you are actually managing is upstream: the guard exists so that a million abusive free calls never reach a vendor that would bill you for them, which at $0.002 a call is the difference between a rounding error and a $2,000 invoice.",1784887028458]