LLM Non-Determinism: What Providers Guarantee, and How to Build Around It

This post is based on sections 3–5 of “Understanding why deterministic output from LLMs is nearly impossible” by Shuveb Hussain at Unstract (Oct. 2025). The material has been rewritten and extended with practical code examples using Pydantic and Snowflake Cortex.

Motivation

When you build a pipeline that sends the same document through an LLM twice and expects the same structured output both times, you will eventually be surprised. Not because you’ve made a mistake, but because LLMs are fundamentally non-deterministic: the same prompt can produce different tokens across runs, even when you set temperature=0.

The root cause is that modern LLMs run on massively parallel GPU hardware, where floating-point arithmetic is not associative. The order in which thousands of parallel threads accumulate intermediate values is not guaranteed to be identical run-to-run, so the final token probability scores shift by tiny amounts. When two candidate tokens are close in probability, that tiny shift can flip which one gets selected. Because LLMs generate text auto-regressively — each token conditions all subsequent tokens — a single early flip can cascade into a structurally different response by the tenth token.

You cannot eliminate this. What you can do is design your system so that it is robust to it. The rest of this article shows you how, concretely, using Pydantic and Snowflake Cortex.

What Providers Actually Promise

No major LLM provider guarantees deterministic output. Snowflake Cortex, which hosts models like mistral-large2 and claude-3-5-sonnet via the complete function in snowflake-ml-python, is no exception. You can observe the problem directly:

from snowflake.cortex import complete
from snowflake.snowpark.context import get_active_session

session = get_active_session()

prompt = (
    "Extract the vendor name and total amount from this invoice: "
    "Acme Corp, Invoice #1042, Total: EUR 3,200.00. "
    "Respond in JSON."
)

run_1 = complete("mistral-large2", prompt, session=session)
run_2 = complete("mistrat-large2", prompt, session=session)

print(run_1)  # {"vendor": "Acme Corp", "total_amount": 3200.00}
print(run_2)  # {"vendor": "Acme Corp", "amount": 3200.0}  ← different field name

The values agree, but the field names differ. A downstream system parsing total_amount will fail silently on the second response.

The reasons providers haven’t solved this are pragmatic. Deterministic GPU operations are substantially slower, routing across a distributed fleet makes identical hardware execution nearly impossible, and most applications tolerate small variations gracefully. The seed parameter, where available, only controls sampling randomness — it does nothing about floating-point drift from parallel reductions, which is the dominant source of variation at temperature=0.

The practical takeaway: treat non-determinism as a fixed environmental property, like network latency. You don’t eliminate it; you engineer around it.

Best Practices for Structured Extraction

Enforce Structure with response_format and Pydantic

The most effective tool available is Snowflake Cortex’s structured output feature. You define your expected output as a Pydantic BaseModel, convert it to a JSON schema with .model_json_schema(), and pass it to complete via CompleteOptions. The model is then constrained to emit output conforming to that schema, and you validate the result back into your Pydantic object.

import json
from pydantic import BaseModel, Field
from snowflake.cortex import complete, CompleteOptions
from snowflake.snowpark.context import get_active_session

class InvoiceExtraction(BaseModel):
    vendor_name: str = Field(description="Full legal name of the vendor")
    invoice_number: str = Field(description="Invoice identifier, e.g. INV-1042")
    total_amount: float = Field(description="Total amount due, numeric only")
    currency: str = Field(description="ISO 4217 currency code, e.g. EUR")

session = get_active_session()

options = CompleteOptions(
    temperature=0,
    response_format={
        "type": "json",
        "schema": InvoiceExtraction.model_json_schema()
    }
)

prompt = "Extract structured data from: Acme Corp, Invoice #1042, Total: EUR 3,200.00"

raw = complete(
    model="mistral-large2",
    prompt=prompt,
    session=session,
    options=options,
)

result = InvoiceExtraction.model_validate_json(raw)

print(result.vendor_name)    # "Acme Corp"
print(result.total_amount)   # 3200.0
print(result.currency)       # "EUR"

The response_format eliminates the most common failure mode: field name variation. The model no longer chooses between total_amount, amount, and sum — the schema decides. The subsequent model_validate_json call gives you a fully typed Python object and raises a ValidationError if anything is malformed.

Write Unambiguous Schemas

The schema is a contract. Vague field descriptions produce vague outputs. Be explicit about types, formats, and what to do when data is missing. Note that Snowflake Cortex has some schema constraints — numeric range keywords like minimum/maximum are not supported, and property names may only contain letters, digits, hyphens, and underscores.

from pydantic import BaseModel, Field
from typing import Optional

class LineItem(BaseModel):
    description: str = Field(description="Product or service name, as written on the invoice")
    quantity: int = Field(description="Number of units. Convert 'dozen' to 12, 'pair' to 2.")
    unit_price: float = Field(description="Price per unit in the invoice currency, numeric only")

class InvoiceExtraction(BaseModel):
    vendor_name: str = Field(description="Full legal name of the vendor")
    invoice_date: str = Field(description="Invoice date in YYYY-MM-DD format. Convert all date formats.")
    total_amount: float = Field(description="Total amount due, numeric only, no currency symbol")
    currency: str = Field(description="ISO 4217 currency code, e.g. EUR, USD, GBP")
    line_items: list[LineItem] = Field(description="All line items listed on the invoice")
    purchase_order_number: Optional[str] = Field(
        default=None,
        description="PO reference number if present on the invoice, otherwise null"
    )

The Optional[str] with default=None on purchase_order_number is important. Without it, the model might hallucinate a PO number when none is present rather than omit the field — a subtle but production-breaking behavior.

Anchor Behavior with Few-Shot Examples

Schema constraints define the structure; few-shot examples define the behavior within that structure. They are especially valuable for edge cases: relative dates, quantity words like “dozen”, and optional fields that should be null. With a simple string prompt, the examples go directly into the prompt text.

from snowflake.cortex import complete, CompleteOptions
from snowflake.snowpark.context import get_active_session

session = get_active_session()

options = CompleteOptions(
    temperature=0,
    response_format={
        "type": "json",
        "schema": InvoiceExtraction.model_json_schema()
    }
)

def build_prompt(invoice_text: str) -> str:
    return f"""You extract structured invoice data. Respond in JSON.

Example 1:
Input: "From: Bolt GmbH | Ref: RE-2024-991 | Date: yesterday | 5x Widget A @ EUR 12.50 | Total: EUR 62.50"
Output: vendor_name="Bolt GmbH", invoice_date="2025-03-24", total_amount=62.50,
        currency="EUR", line_items=[{{description="Widget A", quantity=5, unit_price=12.50}}],
        purchase_order_number=null

Example 2:
Input: "DataPipe Inc | Invoice 5531 | PO: PO-8821 | 1 doz. API calls @ USD 0.01 | USD 0.12 total"
Output: vendor_name="DataPipe Inc", invoice_date=null, total_amount=0.12,
        currency="USD", line_items=[{{description="API calls", quantity=12, unit_price=0.01}}],
        purchase_order_number="PO-8821"

Now extract from:
{invoice_text}"""

raw = complete(
    model="mistral-large2",
    prompt=build_prompt(invoice_text),
    session=session,
    options=options,
)

result = InvoiceExtraction.model_validate_json(raw)

Without the “dozen → 12” example, the model might return quantity=1 with description="dozen API calls". With it, the conversion is consistent.

Measure Variance to Drive Improvement

Variance you don’t measure is variance you can’t improve. Run the same documents through your pipeline multiple times and compare the results. High variance on specific fields points directly at under-specified Field(description=...) strings or missing few-shot examples.

from collections import Counter

def measure_extraction_variance(
    invoice_text: str,
    n_runs: int = 5,
    session=None,
) -> dict:

    results = []
    for _ in range(n_runs):
        result = extract_with_retry(invoice_text, session=session)
        if result:
            results.append(result.model_dump())

    if not results:
        return {"error": "all runs failed"}

    variance_report = {}
    for field in results[0]:
        values = [str(r[field]) for r in results]
        top_value, top_count = Counter(values).most_common(1)[0]
        variance_report[field] = {
            "stable": len(set(values)) == 1,
            "unique_values": list(set(values)),
            "agreement_rate": top_count / n_runs,
        }

    return variance_report

# Example output:
# {
#   "vendor_name":  {"stable": True,  "agreement_rate": 1.0,  ...},
#   "invoice_date": {"stable": False, "agreement_rate": 0.6,  ...},  # ← needs attention
# }

A field with agreement_rate < 0.8 across five runs is a direct signal to improve its description or add a targeted few-shot example. This feedback loop is how schemas and prompts mature over time.

The Right Mental Model

The most useful shift you can make is to stop treating non-determinism as a bug waiting to be fixed and start treating it as a property of the environment — one you design around, not against.

The analogy from distributed systems is apt. TCP/IP is built on top of unreliable packet delivery; the reliability lives in the protocol layer, not in the assumption of a perfect physical network. A reliable LLM pipeline puts its correctness guarantees in the surrounding system — Pydantic validation, retry logic, normalization, business rules — not in the assumption that the model will always produce identical output.

With Snowflake Cortex and snowflake-ml-python, this maps to a clear, implementable layering:

Raw document
      ↓
complete(prompt=..., options=CompleteOptions(response_format=...))   ← flexibility lives here
      ↓
model_validate_json() + retry on ValidationError                     ← structure enforced here
      ↓
NormalizedInvoice model_validator                                    ← canonical field names here
      ↓
Business logic / Snowflake table                                     ← determinism lives here

The complete call is allowed to be flexible — that flexibility is precisely what lets it handle invoice formats you have never seen before. Every layer below it progressively tightens the guarantees, so that by the time data reaches a Snowflake table, it is in a predictable, validated shape regardless of what the model happened to call any given field on that particular run.

Non-determinism is not a problem to eliminate. It is the price of flexibility, and a reasonable one — as long as you don’t ask the model to be your schema enforcer, your validator, and your business-logic layer all at once. Those jobs belong to Pydantic.

Original article: Shuveb Hussain, “Understanding why deterministic output from LLMs is nearly impossible,” Unstract Blog, October 8, 2025. https://unstract.com/blog/understanding-why-deterministic-output-from-llms-is-nearly-impossible/

DB-TASK-002

Get all movies that have rental rate greater than 3
SELECT * FROM film
WHERE rental_rate > 3;

I used this to filter movies which are costly rental movies.

Movies rental rate > 3 and replacement cost < 20
SELECT * FROM film
WHERE rental_rate > 3 AND replacement_cost < 20;

Here I used AND because both condition must be true.

Movies rated PG or rental rate 0.99
SELECT * FROM film
WHERE rating = ‘PG’ OR rental_rate = 0.99;

I used OR because any one condition is enough.

First 10 movies sorted by rental rate highest first
SELECT title, rental_rate
FROM film
ORDER BY rental_rate DESC
LIMIT 10;

I sorted high price first then limited to 10.

Skip first 5 movies and get next 3 sorted by rental rate ascending
SELECT title, rental_rate
FROM film
ORDER BY rental_rate ASC
OFFSET 5
LIMIT 3;

OFFSET used to skip rows.

(same question again so same query)
SELECT title, rental_rate
FROM film
ORDER BY rental_rate ASC
OFFSET 5
LIMIT 3;
Movies with rental duration between 3 and 7
SELECT title, rental_duration
FROM film
WHERE rental_duration BETWEEN 3 AND 7;

BETWEEN is easier than writing >= and <=.

Movies title starts with A and ends with e
SELECT title
FROM film
WHERE title LIKE ‘A%e’;

LIKE used for pattern search.

Customers who do not have email
SELECT first_name, last_name
FROM customer
WHERE email IS NULL;

IS NULL used because = NULL will not work.

Movies released in 2006, rental rate 2.99 or 3.99 and title starts with S, top 5
SELECT title, rental_rate, release_year
FROM film
WHERE release_year = 2006
AND rental_rate IN (2.99, 3.99)
AND title LIKE ‘S%’
LIMIT 5;

IN used instead of multiple OR.

Display 10 customers after skipping first 20 sorted by last name
SELECT first_name, last_name
FROM customer
ORDER BY last_name
OFFSET 20
LIMIT 10;

Used for pagination type query.

Top 5 movies highest replacement cost skipping most expensive one
SELECT title, replacement_cost
FROM film
ORDER BY replacement_cost DESC
OFFSET 1
LIMIT 5;

Offset 1 skips most expensive movie.

Rentals between two dates
SELECT *
FROM rental
WHERE rental_date BETWEEN ‘2005-05-01’ AND ‘2005-06-01’;

Used BETWEEN for date range.

Actors last name contain “man”
SELECT first_name, last_name
FROM actor
WHERE last_name LIKE ‘%man%’;

%man% means contains man anywhere.

Movies where special features is NULL
SELECT title
FROM film
WHERE special_features IS NULL;
Movies where rental duration more than 7
SELECT title, rental_duration
FROM film
WHERE rental_duration > 7;
First 10 movies rental rate 2.99 or 4.99, rating R and title contains L
SELECT title, rental_rate, rating
FROM film
WHERE rental_rate IN (2.99, 4.99)
AND rating = ‘R’
AND title LIKE ‘%L%’
LIMIT 10;
Movies title starts with A or B and ends with s
SELECT title
FROM film
WHERE (title LIKE ‘A%s’ OR title LIKE ‘B%s’);
Movies title contains Man, Men or Woman
SELECT title
FROM film
WHERE title LIKE ‘%Man%’
OR title LIKE ‘%Men%’
OR title LIKE ‘%Woman%’;

Vercel vs Hetzner in 2026: Which Is Actually Worth It for Solo Developers?

Originally published at devtoolpicks.com

A tweet blew up this week asking a simple question: why do developers still pay for Vercel when a €3.79/month Hetzner VPS exists? Nine thousand developers had opinions. Most of them missed the point entirely.

The answer isn’t “Vercel is a scam” or “self-hosting always wins.” The honest answer is: it depends on what you’re building, how much your time is worth, and what stage your project is at.

I’ve deployed on all four of these platforms. Here’s the actual breakdown.

Quick verdict: If you’re shipping a Next.js frontend or a simple SaaS MVP, Vercel is genuinely worth the money. If you’re running a full Laravel or Node backend past early validation, Hetzner with Coolify makes you look silly for not switching. Railway and Render sit in between and serve a specific type of developer well.

Quick Comparison

Platform Starting Price Best For Free Tier
Vercel Free / $20/month Next.js, frontend, JAMstack Yes (non-commercial only)
Railway $5/month Full-stack apps, fast iteration $5 trial credit
Render $7/month Predictable costs, background workers Yes (static + limited web services)
Hetzner From €3.79/month Backend-heavy apps, maximum value No

Vercel

Vercel is the platform Next.js was built for. Literally. Vercel created Next.js. If you’re building anything in that ecosystem, the integration is unmatched.

What it actually costs:
The Hobby plan is free, but it comes with a hard restriction: no commercial use. The moment your project generates revenue, you’re required to upgrade. Pro costs $20 per user per month, with a $20 usage credit included. Add a co-founder and you’re at $40/month before you’ve written a line of code.

Beyond the base fee, you pay for overages. Bandwidth beyond 1 TB costs $0.15/GB. Serverless function execution is billed by CPU time. Image optimization costs extra past the included limit. For most solo projects, these limits are generous. For anything with real traffic or AI workloads, bills can surprise you.

The real pros:

  • Zero-config deploys from GitHub. Push and it’s live
  • Automatic preview URLs for every pull request, which is genuinely useful for client work
  • Global edge CDN with zero configuration required
  • Incremental Static Regeneration and Edge Functions work natively with Next.js
  • Best-in-class Next.js support without workarounds or surprises

The real cons:

  • $20/user/month adds up fast for a bootstrapped project with no revenue yet
  • Hobby plan commercial restriction is enforced via Terms of Service. Vercel does act on violations
  • Vendor lock-in is real. Some Next.js features only work properly on Vercel
  • Usage-based billing means costs get unpredictable at scale
  • It’s the wrong tool for full-stack apps that need persistent storage and background workers

Who should NOT use Vercel:
If you’re building a Laravel app, a Node backend with queues, or anything needing persistent storage and background processes, Vercel is the wrong choice. It’s a frontend-first platform. Forcing a full-stack backend onto Vercel’s serverless model creates complexity and cost that simply doesn’t exist on a VPS.

Railway

Railway is what Heroku should have become. It’s a modern PaaS that gives you the deployment simplicity of Vercel but for full-stack apps: backends, databases, workers, and cron jobs in one place.

What it actually costs:
The Hobby plan is $5/month, which includes $5 of resource usage. For very light projects, it’s effectively free. Pro is $20/month with a $20 usage credit. Railway bills by the minute for actual compute used. If your app idles, you pay almost nothing. The visual project canvas showing all your services connected together is one of the nicest developer interfaces available right now.

The real pros:

  • Fastest deploy experience of any platform on this list
  • Visual canvas shows your entire architecture (app, DB, workers) at a glance
  • Usage-based billing means low-traffic side projects cost almost nothing
  • Supports any language via Docker or Nixpacks
  • Excellent for Laravel, Node backends, Python APIs, anything full-stack

The real cons:

  • Pricing gets unpredictable as usage scales. Hard to budget to an exact number
  • Less mature than Render for multi-region setups
  • Free tier is a one-time $5 trial credit, not a permanent option
  • Documentation is thinner than Render’s

Who should NOT use Railway:
If you need predictable fixed monthly costs, Railway’s usage-based model requires active monitoring to avoid surprises. It’s also not ideal for static site hosting. Use Vercel or Netlify for that. And if you’re running a high-traffic production app needing multi-region redundancy, Railway isn’t ready for that yet.

Render

Render is the most Heroku-like option on this list. Fixed pricing tiers, a permanent free option for static sites, and a platform that’s been reliable for years. It’s not the cheapest or the most exciting, but it causes the least drama.

What it actually costs:
Static sites are free forever. Web services start at $7/month for the Starter tier (512MB RAM, 0.5 CPU). Standard is $25/month (2GB RAM, 1 CPU). For a solo dev running a web service plus a PostgreSQL database, expect to pay roughly $20-35/month depending on database size.

The real pros:

  • Permanent free tier for static sites, no 30-day expiry
  • Predictable pricing makes monthly budgeting straightforward
  • Built-in background workers, cron jobs, and PostgreSQL without extra configuration
  • More mature documentation than Railway
  • Better multi-region support than Railway

The real cons:

  • $7/month minimum for always-on backend services
  • Free PostgreSQL databases expire after 30 days. This catches people out more often than it should
  • Slower deployments than Railway in head-to-head tests
  • The interface feels more enterprise-y and less modern than Railway’s

Who should NOT use Render:
If deploy speed and iteration pace matter most (frequent changes, lots of preview environments), Railway feels sharper day-to-day. And if your primary workload is a Next.js frontend, just use Vercel. Render adds nothing in that scenario.

Hetzner

This is the option that started the Twitter debate. For the right developer at the right project stage, it’s not even close.

What it actually costs:
The CX22 gives you 2 vCPU, 4GB RAM, 40GB NVMe SSD, and 20TB monthly traffic for €3.79/month. The CX32 gives you 4 vCPU, 8GB RAM, and 80GB storage for €6.80/month. These aren’t cloud credits with usage surprises. They’re fixed specs that stay yours as long as you’re paying.

Pricing heads-up: Hetzner has confirmed a price increase effective April 1, 2026. Some plans will increase. Check the current pricing at hetzner.com/cloud before signing up. Even after the adjustment, Hetzner remains significantly cheaper than comparable specs on most platforms.

For comparison: a DigitalOcean 2 vCPU / 4GB droplet costs $24/month, roughly 4-6x more than Hetzner for equivalent specs. DigitalOcean wins on US-based support, better beginner documentation, and a larger tutorial ecosystem. If you’re new to self-hosting and based in the US, DigitalOcean is a gentler entry point. If you know what you’re doing, Hetzner is the obvious value choice.

The real pros:

  • Best price-to-spec ratio in the VPS market, full stop
  • 20TB monthly traffic included in EU regions. Competitors charge extra for this
  • GDPR-compliant EU data residency out of the box, relevant for EU founders
  • Pair it with Coolify and you get a self-hosted PaaS for about €4-7/month total
  • No vendor lock-in, no serverless limits, no surprise bills

The real cons:

  • You manage the server. Security patches, updates, monitoring are all on you
  • No managed database, no formal uptime SLA
  • Support is ticket-based only for cloud products. No live chat, no phone
  • US datacenter traffic drops to 1TB/month compared to 20TB in EU regions
  • Setup takes a few hours to go from fresh VPS to production-ready. Not minutes

Who should NOT use Hetzner:
If you’re shipping a Next.js frontend and iterating fast, the time saved with Vercel’s zero-config deploys is real. Hetzner is not for developers who want to avoid thinking about servers. If nginx configs make you uncomfortable, use a managed platform. Also not ideal if your users are primarily in the US and latency matters.

Head-to-Head: The Decision That Actually Matters

Most comparisons get this wrong by treating everyone as the same type of developer. The question isn’t “which platform is best” but “which platform is right for where you are right now.”

For a Next.js SaaS MVP in the first six months:
Vercel wins. Zero-config deploys, preview URLs, and native ISR save real time when you’re iterating daily. At $20/month, it’s cheaper than the hours you’d spend configuring a VPS for a frontend-heavy app. Reassess when you hit $1-2K MRR and the cost starts to feel meaningful.

For a full-stack Laravel or Node app:
Railway or Hetzner. Vercel is genuinely the wrong tool here. It’s built for serverless frontends, not persistent backends. Railway gets you to production faster. Hetzner gets you there cheaper once you’ve validated the idea.

For a bootstrapped SaaS past early validation:
Hetzner with Coolify. I covered the full setup in Laravel Forge vs Ploi vs Coolify. A €6.80/month CX32 running your app, database, queue worker, and reverse proxy is genuinely hard to justify moving away from. The Vercel equivalent would cost 4-5x more.

For a solo dev who hates DevOps:
Render. Not the cheapest or most exciting, but it’s the most reliable “just works” platform for full-stack apps if you don’t want to think about servers.

Final Recommendation

Use Vercel if you’re building a Next.js app, developer experience matters more than cost right now, and you’re in early validation mode where $20/month is irrelevant.

Use Railway if you want the managed PaaS experience for a full-stack app, you prefer paying for actual usage over a flat fee, and you value a modern, fast deploy workflow.

Use Render if you want predictable monthly costs, you’re running background workers or cron jobs, and you value platform stability and clear documentation over cutting-edge features.

Use Hetzner if you’re past early validation, comfortable with Linux, want the best raw compute value on the market, and are running a backend-heavy app that doesn’t need Vercel’s serverless features.

The viral debate framed this as “Vercel is wasteful.” That’s not quite right. Vercel is expensive for the wrong use case. For a Next.js app in early growth, it earns its cost. The mistake most indie hackers make is staying on Vercel well past the point where switching makes obvious financial sense.

FAQ

Is Vercel free for commercial use?
No. Vercel’s Hobby plan is free but explicitly restricted to personal, non-commercial projects. Any project that generates revenue requires the Pro plan at $20/user/month. Vercel enforces this through their Terms of Service.

Is Hetzner good for beginners?
Hetzner is a raw VPS. You configure it yourself. It’s not beginner-friendly out of the box. Pairing it with Coolify gives you a GUI for deployments, SSL, and database management that makes self-hosting far more approachable. DigitalOcean is also a gentler starting point if you’re new to servers.

What’s the cheapest way to host a Laravel app in 2026?
A Hetzner CX22 (check current pricing after the April 1 increase) with Coolify installed for free is the cheapest production-ready option. A CX32 handles most small-to-medium Laravel SaaS workloads comfortably for under €7/month.

Can Railway replace Vercel?
For Next.js apps specifically, no. Vercel’s native integration is too deep. For everything else, including Node backends, Laravel, Python, and Go, Railway is a legitimate and often better alternative with a great developer experience.

Does Hetzner have US data centres?
Yes. Hetzner has US servers in Ashburn, Virginia and Hillsboro, Oregon. The included monthly traffic drops from 20TB to 1TB in US regions compared to EU, which is worth factoring in for bandwidth-heavy applications.

Conclusion

This isn’t a fight between “Vercel is overpriced” and “self-hosting always wins.” It’s a question of stage, stack, and how you want to spend your time.

Use managed platforms early. They buy you speed when speed matters most. Switch to Hetzner when the savings become real money and your setup has stabilised. The tweet that went viral this week wasn’t wrong. But the 9,000 replies proved that context matters more than any single verdict.

Most indie hackers make that switch too late. Don’t be one of them.

Building a Production-Aware AI Backend with FastAPI

Most AI backend examples stop at one thing:

send a prompt, get a response.

That is fine for demos, but real systems usually need more than that.

Once you try to use AI inside an actual product, a few practical questions show up immediately:

  • How much does each request cost?
  • How long does each response take?
  • Can we stream output instead of waiting for a full response?
  • Can we reduce hallucinations by grounding responses in known data?
  • Can we log usage for billing and analytics?

I wanted to build something closer to that reality.

So instead of making another thin OpenAI wrapper, I built a FastAPI-based AI backend with:

  • synchronous responses
  • streaming responses
  • usage logging
  • token-based cost estimation
  • response time monitoring
  • lightweight context-based answering
  • Docker reproducibility

The result is a backend that feels much closer to something you could actually extend into an internal AI tool or SaaS feature.

Why I Built It This Way

A lot of AI tutorials focus on model output.

I wanted to focus on backend behavior.

That means:

  • responses should be explainable
  • system behavior should be predictable
  • logs should support observability
  • the backend should be structured for extension, not just for demo screenshots

In other words, I was less interested in “Can this call OpenAI?”
and more interested in “Can this behave like a real backend feature?”

Tech Stack

  • Python 3.11
  • FastAPI
  • SQLAlchemy 2.0
  • Alembic
  • OpenAI API
  • SQLite
  • Docker

What the Backend Does

The project currently includes:

  • POST /ai/test for standard AI responses
  • POST /ai/stream for streaming output
  • POST /ai/upload for adding text-based context data
  • POST /seed for inserting sample context
  • GET /ai/logs for inspecting stored usage logs

It also stores request-level data such as:

  • prompt
  • response
  • total tokens
  • estimated cost
  • response time
  • endpoint name
  • user id
  • timestamp

That logging layer turned out to be one of the most important parts of the project.

Because once you can see how AI is being used, the backend starts becoming operational instead of experimental.

Lightweight RAG-Style Answering

One of the goals was to reduce hallucinated answers.

Instead of letting the model answer freely from its general knowledge, I added a lightweight retrieval flow:

  1. search relevant records from the database
  2. inject the retrieved content into the prompt
  3. instruct the model to answer only from that context

This is not a full vector database setup.
It is intentionally lightweight.

The retrieval logic uses:

  • simple keyword-based matching
  • basic query pre-processing for Japanese text
  • AND search first
  • OR fallback if needed
  • safe fallback responses when no context is found

So the project is better described as a lightweight RAG-style backend rather than a full enterprise retrieval system.

That was deliberate.

I wanted something small enough to understand, but structured enough to feel useful.

Why Streaming Matters

Streaming changes the feel of an AI product a lot.

Without streaming, the user waits for the full answer.
With streaming, the user gets feedback immediately.

That makes the backend feel much closer to a real assistant feature.

So I added /ai/stream and then made sure streaming requests were not treated like second-class citizens.

I wanted them logged too.

That meant tracking:

  • total tokens
  • estimated cost
  • response time
  • endpoint name

This was important because a lot of examples show streaming output, but do not show how to observe or measure it properly.

In practice, that observability layer is what makes the feature maintainable.

Cost Tracking and Latency Monitoring

For regular /ai/test responses, token usage was straightforward to capture.

For streaming, it required a bit more work.

I refactored the provider layer so the streaming flow could still capture usage data at the end, then calculate an estimated cost and store it together with the final response log.

That gave me a much more useful log structure.

Instead of only storing “prompt” and “response,” I could now see:

  • how many tokens were used
  • how much the request approximately cost
  • how long the request took
  • which endpoint generated it

That is a much stronger foundation for:

  • cost visibility
  • future billing models
  • usage analytics
  • performance monitoring

One Practical Issue I Hit

Alembic autogeneration tried to include unrelated schema changes while I was extending the logging table.

It detected the new columns I wanted:

  • estimated_cost
  • response_time_ms
  • endpoint

But it also tried to remove an unrelated documents table.

That was a good reminder that migration generation is helpful, but not magical.

I manually cleaned the migration so it only included the actual intended schema change.

That was one of those small but very real backend moments:
not “how do I make the feature work?”
but “how do I make the change safe?”

Current Logging Model

The request log now stores:

  • prompt
  • response
  • total_tokens
  • estimated_cost
  • response_time_ms
  • endpoint
  • user_id
  • created_at

That makes the backend feel much more production-aware than a simple AI demo.

What This Project Is Really About

The most important thing I learned is that AI backend work is not just model integration.

It is also about:

  • structure
  • safety
  • logging
  • reproducibility
  • monitoring
  • extension paths

Calling an API is easy.

Building something that behaves predictably when it grows is the harder part.

That is what I wanted this project to reflect.

What I Would Add Next

The next natural steps would be:

  • JWT authentication
  • token quota control
  • admin-facing usage analytics
  • Stripe integration
  • richer retrieval strategies
  • vector-based search when the use case really needs it

But even in its current form, the backend already demonstrates something important:

AI features become much more valuable when they are treated as backend systems, not just model calls.

Repository

GitHub: fastapi-ai-core

The repository includes Docker setup, logging examples, and a lightweight context-retrieval flow.

If you are building AI-enabled backend systems with FastAPI, I think this kind of structure is worth caring about early.

Because once usage grows, observability stops being a nice-to-have.
It becomes part of the product itself.

WLBS + MoE: Finally, an AI that learns where the bug really lives

You’ve seen this happen.

An AI coding agent tries to fix an ImportError in rbac.py. It edits the file three times, five times, ten times. Nothing works.

The real bug is in roles.py—the file that rbac.py imports. The error shows downstream, but the cause lives upstream.

The AI keeps banging on the wrong file because its memory is temporal, not spatial. It remembers “I failed here” but not “this file depends on that one.”

We built a system that fixes this gap—and it works.

The core idea: keep information in its original shape
Most AI agents today store experience as text: rules extracted from logs, reflections written in natural language. Every translation discards structure. The call graph, the dependency chain, the propagation path—all flattened into words.

WLBS (World-Line Behavior Space) does the opposite.

  • Every function, module, or service node keeps a world-line—an append‑only log of every event that ever touched it. Task ID, action, result, reasoning, whether that reasoning was correct—all stored on the node itself.
  • When a test fails, a curvature increment propagates backward along the call graph. Nodes closer to the root cause get larger increments; after repeated failures, the upstream root cause accumulates the highest curvature.
  • A singularity flag appears on that node—no natural language, no parsing, just a typed attribute the decision layer reads directly.
  • Resolution decay (inspired by the biological fovea) feeds the LLM only what it needs: full history for nearby nodes, key events for the neighbourhood, just identifiers and curvature for distant parts. Context stays small—about 375 tokens for a real cross‑file bug.

No translation, no retrieval, no guesswork. The geometry of the dependency graph itself points to the root cause.

MoE meets WLBS: each node gets its own expert
WLBS gives the system structural memory. MoE (Mixture of Experts) gives it specialised intelligence.

We map every node in the behavior graph to a lightweight expert (a LoRA adapter). A tiny router reads the node’s curvature and world‑line summary, then activates the relevant experts—often the current node plus high‑curvature upstream nodes.

The loop:

  1. A test fails at node rbac.py.
  2. Curvature propagates upstream; roles.py gets a high curvature increment.
  3. Router sees the high curvature upstream and activates both the roles.py expert and the rbac.py expert.
  4. Experts produce a decision: modify roles.py (not rbac.py).
  5. Action is executed, feedback is written back to the world‑lines.
  6. Success → curvature decays for rbac.py and increments for roles.py are already there.
  7. Offline, high‑curvature nodes are fine‑tuned (LoRA) using their world‑line data—only the nodes that actually matter get updated.

The result: continuous improvement without full retraining. The system gets better at diagnosing similar issues every time it encounters them, and the learning is isolated to the nodes that actually need it.

Real numbers from a real bug
We tested on a cross‑file repair task (b‑16: roles.py → rbac.py).

  • Behavioral distance correctly identified as 1 hop.
  • Context assembly took 5–22 ms.
  • Gate prompt size ~375 tokens.
  • First effective write target was roles.py—the correct upstream file.
  • Passing tests went from 34/44 → 42/44.
  • Gate failure rate 4.76%.

And the improvement came without parameter updates—just from world‑line accumulation and curvature propagation.

Why this matters for developers

  • **No more chasing symptoms. **You get spatial root‑cause attribution without hand‑coded rules.
  • Low overhead. The routing prompt is tiny; the curvature propagation is pure arithmetic.
  • Works with existing LLMs. Gate is a standard LLM, fed structured data (graph + curvature + world‑line excerpts). You don’t need to retrain the model.
  • **Scales. **Resolution decay keeps context bounded; experts are updated only when their curvature spikes.
  • Opens the door to other domains. The same pattern works for microservices, supply chains, even clinical diagnosis—anywhere there’s a dependency graph and repeatable failures.

What’s next
We’ve filed two Chinese patents (CN 2026103746505 and CN 2026103756225) and are releasing the core implementation open‑source (repo to be announced). A preprint is on arXiv (cs.SE / cs.AI, March 25, 2026).

We’re actively looking for:

Early adopters to try WLBS on their own codebases or microservice topologies.

Contributors to help with runtime dependency tracking, large‑graph performance, and parameter auto‑tuning.

Researchers to extend the framework to new domains and formalise the curvature propagation theory.

Get involved

Watch the GitHub repo :https://github.com/val1813/wlbs-cli
Try the CLI prototype: pip install wlbs-scan

If you’ve ever spent hours chasing a bug that was upstream, or wished your AI agent had a sense of space, this is for you.

Let’s give AI a map—not just a timeline.

I Built a Free PDF Toolkit in Go + Astro — Here’s What I Learned

Every few months I find myself needing to merge two PDFs or pull a few pages out of a big document. I open a search, find a tool, and immediately get hit with “sign up to continue” or “buy premium to process files over 5MB.” For a 30-second task.

So I built PDFCrush — a free online PDF toolkit that does merge, split, and compress without accounts or upsells. Here is what the process looked like and what I learned along the way.

The Stack

  • Backend: Go with the chi router
  • Frontend: Astro with Tailwind CSS
  • Database: SQLite via modernc.org/sqlite
  • PDF processing: pdfcpu (pure Go)
  • Hosting: Hetzner ARM64 VPS, Caddy reverse proxy, Cloudflare

No frameworks on the backend. No ORM. No Redis. The entire thing is a single Go binary serving both the API and the static frontend.

Why Go?

I wanted a single binary I could scp to a cheap ARM64 VPS and run. No runtime, no dependency hell, no node_modules on the server. Go gives me that plus genuinely good concurrency for handling file uploads.

The server setup is minimal — chi for routing, slog for structured JSON logging, and standard library for everything else:

r := chi.NewRouter()

r.Use(chimw.RequestID)
r.Use(chimw.RealIP)
r.Use(chimw.Logger)
r.Use(chimw.Recoverer)
r.Use(middleware.SecurityHeaders)
r.Use(middleware.CORS("*"))

r.Route("/api/v1", func(r chi.Router) {
    r.Get("/health", handlers.HealthCheck(store))

    // PDF operations — rate limited for free users.
    r.With(rateLimiter.Limit).Post("/pdf/merge", pdfHandlers.Merge)
    r.With(rateLimiter.Limit).Post("/pdf/split", pdfHandlers.Split)
    r.With(rateLimiter.Limit).Post("/pdf/compress", pdfHandlers.Compress)
})

Chi’s middleware chaining with .With() is one of those things that looks obvious but makes route-level concerns (rate limiting, auth) really clean.

Pure Go PDF Processing (No CGO)

This was the decision that shaped the whole project. Most PDF libraries in Go lean on C bindings (poppler, MuPDF, etc.), which means CGO, which means cross-compilation headaches and no static binaries on ARM64 without pain.

Instead I used pdfcpu, which is pure Go. The processing layer is thin:

func (p *Processor) Merge(files []string) (string, error) {
    if len(files) < 2 {
        return "", fmt.Errorf("pdf: merge requires at least 2 files, got %d", len(files))
    }

    outFile := filepath.Join(p.uploadDir, uuid.New().String()+".pdf")

    conf := model.NewDefaultConfiguration()
    if err := api.MergeCreateFile(files, outFile, false, conf); err != nil {
        return "", fmt.Errorf("pdf: merge failed: %w", err)
    }

    slog.Info("pdf merge completed", "input_count", len(files), "output", outFile)
    return outFile, nil
}

Split and compress follow the same pattern — validate inputs, call pdfcpu, return a path. The Processor struct owns the upload directory and max file size, and every handler cleans up temp files with defer:

defer h.processor.Cleanup(savedPaths...)

The trade-off: pdfcpu does not do image resampling for compression, so “compress” is really “optimize the PDF structure” (remove duplicate objects, linearize streams). For most office-generated PDFs this still cuts 20-40% off the size. For image-heavy PDFs, less so. Honest about it, and it still beats “please sign up first.”

SQLite for Everything

I use modernc.org/sqlite — a pure Go translation of SQLite’s C source. No CGO needed. It works on ARM64, M-series Macs, and Linux AMD64 without any build flags.

db, err := sql.Open("sqlite", dbPath)

pragmas := []string{
    "PRAGMA journal_mode=WAL",
    "PRAGMA foreign_keys=ON",
    "PRAGMA busy_timeout=5000",
    "PRAGMA synchronous=NORMAL",
}

SQLite handles rate-limit counters, operation logs, subscriptions, and email signups. For a tool that processes maybe a few hundred requests a day, it is absurd overkill to reach for Postgres. WAL mode gives me concurrent reads while writes are happening, and the entire database is a single file I can back up with cp.

Rate Limiting Without Redis

The rate limiter is SQLite-backed. It counts operations per IP within a sliding window:

type RateLimiter struct {
    store     *storage.Store
    maxOps    int
    window    time.Duration
    proSecret string
}

func (rl *RateLimiter) Limit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := extractIP(r)

        // Pro users bypass rate limiting.
        proEmail := proauth.ReadProEmail(r, rl.proSecret)
        if proEmail != "" {
            isPro, _ := rl.store.IsProUser(r.Context(), proEmail)
            if isPro {
                next.ServeHTTP(w, r)
                return
            }
        }

        since := time.Now().Add(-rl.window)
        count, _ := rl.store.GetOperationCount(r.Context(), ip, since)

        if count >= rl.maxOps {
            w.Header().Set("Retry-After", fmt.Sprintf("%d", int(rl.window.Seconds())))
            writeJSONError(w, http.StatusTooManyRequests,
                fmt.Sprintf("rate limit exceeded: %d operations per %s", rl.maxOps, rl.window))
            return
        }

        rl.store.RecordOperation(r.Context(), ip)
        next.ServeHTTP(w, r)
    })
}

Free users get 5 operations per 24 hours. Pro users (identified by an HMAC-signed HTTP-only cookie) bypass the limit entirely. No Redis, no in-memory maps that reset on deploy — the counts survive restarts because they live in SQLite.

Security Without Overthinking It

A middleware function sets the standard hardening headers:

func SecurityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        w.Header().Set("Content-Security-Policy",
            "default-src 'self'; script-src 'self' 'unsafe-inline'; ...")
        next.ServeHTTP(w, r)
    })
}

Uploaded files get validated with magic bytes (%PDF- header check) and saved with UUID filenames to prevent path traversal. Files are deleted immediately after the response is sent. There is also a background cleanup job that sweeps stale uploads every hour, just in case.

Why Astro for the Frontend

Astro ships zero JavaScript by default. For a tool where the landing page is static marketing content and the actual app is one interactive page, that is perfect. The landing page loads fast (just HTML + CSS), and the interactive PDF uploader is a React island hydrated only on the app page.

The entire frontend builds to static files that the Go server serves directly — no separate Node process in production.

What I Would Do Differently

Start with better compression. pdfcpu’s optimize is solid for structural cleanup but does not resample images. If I were starting over, I would evaluate whether Ghostscript via a subprocess (ugly but effective) is worth it for the compress feature specifically.

Skip the dual payment provider setup. I wired up both Stripe and LemonSqueezy. In practice, Stripe handles everything. The abstraction cost was not worth the optionality.

Add WebSocket progress for large files. Right now, large merges just hang with a spinner until the response comes back. A progress stream would make the UX feel faster even if the processing time is the same.

The Numbers

The whole thing runs on a single Hetzner CAX21 (ARM64, 4 vCPUs, 8GB RAM) that costs about 7 EUR/month — and that VPS also hosts two other products. Memory usage hovers around 30MB for the Go process. Deploys are rsync + docker build on the VPS, done in under a minute.

Try It

If you need to merge, split, or compress PDFs, give it a try at pdfcrush.dev — it is free, no signup required, and your files are deleted immediately after processing.

The source is part of a larger monorepo where I am experimenting with AI agents that build and manage products autonomously, but that is a story for another post.

How to Verify US Business Entities Across States Without Paying $500/mo for an API

If you work in compliance, sales, or risk, you’ve probably needed to verify whether a business is actually registered, who its officers are, or when it was formed.

The commercial APIs that do this — Cobalt Intelligence, OpenCorporates, Dun & Bradstreet — charge $300-500/month minimum. Some charge per lookup. For startups, small compliance teams, or anyone doing occasional due diligence, that’s hard to justify.

Here’s the thing: all of this data is public. Every US state maintains a Secretary of State (or equivalent) database where business registrations are filed. The information is free to access. The problem is that each state has its own portal, its own search interface, its own data format, and its own quirks.

I built free-to-use actors on Apify that let you search the four largest state business registries through a single, consistent interface. No API keys. No subscriptions. Pay only for the results you use.

What you get

Each actor returns structured data including:

  • Entity name and entity number
  • Entity type (LLC, Corporation, LP, etc.)
  • Formation/registration date
  • Status (Active, Dissolved, Suspended, etc.)
  • Registered agent name and address
  • Officers/directors (where available)
  • Principal address
  • Jurisdiction of formation
  • Filing history (where available)

All returned as clean JSON. Export to CSV, Excel, Google Sheets, or consume via API.

The four states

These cover roughly 40% of all US business entities:

California Secretary of State

Search by entity name or entity number. Returns formation date, status, agent for service of process, and entity type. California has 4+ million active business entities.

Try it on Apify

Texas Comptroller of Public Accounts

Search by entity name, taxpayer number, or SOS file number. Returns officers, registered agent, EIN, addresses, franchise tax status. Texas has 3+ million active entities.

Try it on Apify

New York Division of Corporations

Search by entity name or DOS ID. Returns service of process info, CEO, registered agent, stock details, filing dates, jurisdiction. New York has 2+ million active entities.

Try it on Apify

Florida Division of Corporations

Access bulk filing data from the public Sunbiz SFTP server. Parse daily or quarterly data dumps with officers, registered agent, addresses, annual reports. Florida has 5+ million entities on record.

Try it on Apify

Use cases

KYC/AML compliance: Verify that a business counterparty is actually registered and in good standing before onboarding. Cross-check officer names against what the business self-reported.

Sales prospecting: Build targeted lists of newly formed LLCs or corporations in specific states. Filter by formation date to find businesses less than 90 days old — they’re actively buying services.

Due diligence: Before an investment, acquisition, or partnership, verify the target company’s registration status, officers, and agent for service of process across every state where they claim to operate.

Legal research: Find registered agent information for service of process. Identify all entities associated with a particular officer or address.

Competitive intelligence: Track when competitors form new entities, change officers, or register in new states.

How it compares to paid APIs

Feature Commercial APIs ($300-500/mo) These Apify actors
States covered 50 states CA, TX, NY, FL (40% of entities)
Pricing Monthly subscription Pay per result ($0.005/result)
Minimum commitment Monthly or annual None
Bulk export Usually extra Included
API access Yes Yes (Apify API)
Freshness Varies Real-time from state portals
Integrations Custom Zapier, Make, Google Sheets, Slack, Airtable

For teams that only need the four largest states, the cost difference is significant. A 1,000-entity verification batch costs about $5 instead of $300+/month.

Getting started

  1. Go to any of the actor pages linked above
  2. Enter a business name or entity number
  3. Click “Start” — results appear in seconds
  4. Export as JSON, CSV, or Excel

For programmatic access, use the Apify API to trigger runs and retrieve results. Works with any language that can make HTTP requests.

What’s next

I’m expanding to more states. Illinois, New Jersey, Ohio, Pennsylvania, and Georgia are in the pipeline. The goal is all 50 states through a single, consistent interface.

If there’s a specific state you need, let me know in the comments — I’ll prioritize based on demand.

All actors: apify.com/pink_comic

Idempotency Situation

The problem is
Same transaction may be executed multiple times
This can happen due to network retry or duplicate request
This may lead to wrong balances

Initial Data
Alice has 1000
Bob has 500

Normal Transfer
Transfer 200 from Alice to Bob

After transaction
Alice becomes 800
Bob becomes 700

Duplicate Execution
Same transfer runs again

After second execution
Alice becomes 600
Bob becomes 900

Transaction is applied again

Database allows repeated execution
It does not know whether request is duplicate
Each execution is treated as a new operation

Because database only ensures consistency per transaction
It does not track duplicate requests
There is no built in mechanism to stop same transfer again

Database ensures No partial updates and Valid data state

Use unique transaction id for each request
Check if transaction already exists before processing
Reject duplicate requests

Database ensures correctness of each transaction
Application ensures transaction is not repeated

I built a simple IP checker tool

I’ve been working on a simple IP checker that shows your IP address, location, ISP, and browser info.

The idea was to keep it clean and easy to understand, especially for beginners who want to know what their IP actually reveals.

While building it, I also realized how confusing the basics of the internet can be for most people — things like DNS, servers, and how a page actually loads.

So I wrote a simple breakdown explaining how websites actually work, step by step.

If you’re curious, you can check it here:
https://examineip.com/how-websites-actually-work-simple-explanation-for-beginners/

And here’s the tool itself:
https://tools.examineip.com/

Would love feedback or suggestions on what to improve.

11 Free Government Data APIs You Didn’t Know Existed (And How to Use Them)

The US government publishes enormous amounts of data through public APIs — SEC filings, vehicle recalls, consumer complaints, federal contracts, healthcare provider registries, drug safety reports, and more.

The problem: these APIs are scattered across dozens of agencies, each with its own authentication scheme, rate limits, pagination quirks, and data format. Finding the right endpoint, figuring out the request format, and parsing the response takes hours of documentation reading before you get a single useful result.

I built ready-to-use actors on Apify for 11 of the most valuable government data sources. Each one handles the API quirks, pagination, and data formatting so you get clean, structured JSON output you can export to CSV, connect to Google Sheets, or pipe into your app via API.

All of these use free public APIs. No API keys required.

1. SEC EDGAR — Company Filings Search

Search SEC filings by company name, stock ticker, or CIK number. Filter by form type (10-K, 10-Q, 8-K, Form 4) and date range.

Who uses this: investors doing due diligence, compliance teams tracking competitor filings, financial analysts monitoring insider transactions.

Example: Search “TSLA” to get all Tesla SEC filings with direct links to the actual documents.

Try it on Apify

2. USAspending — Federal Contracts & Grants

Search $7+ trillion in federal spending data. Find government contracts, grants, and loans by keyword, agency, recipient, state, NAICS code, or date range.

Who uses this: government contractors tracking competitors, BD teams finding opportunities, journalists investigating federal spending, researchers studying procurement patterns.

Example: Search for “cybersecurity” contracts from the Department of Defense to see who’s winning federal cyber work.

Try it on Apify

3. CFPB — Consumer Financial Complaints

Search 2+ million consumer complaints against banks, credit card companies, mortgage lenders, and debt collectors. Filter by company, product, issue, state, and date.

Who uses this: compliance teams monitoring complaint trends, fintech companies identifying pain points, investors assessing reputation risk, attorneys researching patterns of consumer harm.

Example: Search “WELLS FARGO” to see all complaints, filtered by product type and time period.

Try it on Apify

4. NHTSA — Vehicle Safety Recalls

Look up vehicle recalls by make, model, and year. Get full recall details: affected component, safety consequence, remedy, and whether it’s a “Park It” (do not drive) advisory.

Who uses this: used car buyers, auto dealers verifying inventory, fleet managers, insurance companies, automotive journalists.

Example: Search “Tesla Model 3 2024” to find all active recalls.

Try it on Apify

5. Federal Register — Rules, Regulations & Executive Orders

Search the daily journal of the US government. Find proposed rules, final rules, notices, and presidential documents by keyword, agency, date, or CFR reference.

Who uses this: regulatory affairs teams tracking new rules, lobbyists monitoring proposed regulations, compliance officers, policy researchers.

Example: Search EPA proposed rules from the last 90 days to see what environmental regulations are coming.

Try it on Apify

6. NPPES NPI Registry — Healthcare Provider Lookup

Search 8+ million healthcare providers in the National Plan and Provider Enumeration System. Find doctors, hospitals, and clinics by name, specialty, location, or NPI number.

Who uses this: health tech companies building provider directories, insurance companies verifying credentials, healthcare recruiters, patients looking up provider details.

Example: Search for cardiologists in California to get NPI numbers, addresses, phone numbers, and taxonomy codes.

Try it on Apify

7. openFDA — Drug Safety Data

Search FDA drug adverse events, recalls, and drug labels. Three search modes cover different aspects of pharmaceutical safety data.

Who uses this: pharma companies monitoring adverse events, regulatory teams tracking recalls, researchers studying drug safety profiles, health journalists.

Example: Search adverse events for “Ozempic” to see reported side effects and outcomes.

Try it on Apify

State Business Entity Registrations

Business entity data is one of the most in-demand public records categories. KYC/compliance teams, sales prospecting, due diligence, and competitive intelligence all need it. I built actors for the four largest states:

8. California Secretary of State

Search CA business entities by name or entity number. Get formation date, status, agent for service of process, and entity type.

Try it on Apify

9. Texas Comptroller

Search TX business entities by name, taxpayer number, or SOS file number. Full details: officers, registered agent, EIN, addresses, franchise tax status.

Try it on Apify

10. New York Division of Corporations

Search NY business entities by name or DOS ID. Full details: service of process, CEO, registered agent, stock info, filing dates, jurisdiction.

Try it on Apify

11. Florida Division of Corporations

Access FL bulk filing data from the public Sunbiz SFTP server. Parse daily or quarterly data dumps with officers, registered agent, addresses, annual reports.

Try it on Apify

Why Apify?

Each of these actors runs in the cloud — no infrastructure to manage, no dependencies to install, no rate limit headaches. You get:

  • Structured output — clean JSON with consistent field names
  • Multiple export formats — JSON, CSV, Excel, or direct API access
  • Integrations — connect to Google Sheets, Slack, Airtable, Zapier, Make, and more
  • Scheduling — run daily/weekly to keep your data fresh
  • Pay per result — no monthly subscriptions, just pay for the data you use

What’s next

Since writing this, I’ve shipped 19 more actors — CPSC product safety recalls, CMS Nursing Home Compare, USDA FoodData Central, FDIC BankFind (27,800+ bank institutions), U.S. Census ACS (demographics by any geography), IRS 990 nonprofit financials, Medicare Part D drug spending, FEC campaign finance, EPA toxic releases, ClinicalTrials.gov, BLS Economic Data (CPI, unemployment, wages), NIH RePORTER research grants, FEMA Disaster Declarations, USGS Earthquake Search, World Bank Indicators, and PubChem Compound Search. 30 actors total, all free to try. If there’s a government data source you need access to, let me know in the comments.

All actors are available at apify.com/pink_comic.