Vercel is serving MiniMax M3.1 Flash for free until 22 October [2]. Free model, short window. So obviously I built something with it. 🛠️
The idea is simple. Let the model write SQL. Never let it run SQL. A small local app takes the query, runs it on read-only sample data, and shows you the report. The app rejected the six disallowed queries I tested below.
No API key in the local app. Generation uses a signed-in Vercel playground; the app itself makes no inference requests.
The finished app running a query MiniMax actually wrote. Sample data is made up: twelve fake orders.
What I'm building
Meet SQL Postcard. Two boxes:
- Generate: you copy a prompt into Vercel's MiniMax playground and get SQL back.
- Check & run: you paste that SQL into the app. It runs on a fresh in-memory SQLite database with twelve fake orders.
That copy-paste step looks old-school, and it's on purpose. The model never touches your data. It only sees the table's shape and the question.
Step 1: Run the app 🧪
You need Python 3 and nothing else. The app only uses the built-in sqlite3 module. I tested it on Python 3.14.7.
Get SQL Postcard on GitHub. Clone it and open the project folder:
git clone https://github.com/panditshivam/sql-postcard.git
cd sql-postcard
python3 verify.py
python3 app.py --port 8775
verify.py checks the saved queries against a plain Python calculation. app.py starts the app at http://127.0.0.1:8775. Neither command calls a model.
Step 2: Get SQL from MiniMax for free
Open the MiniMax M3.1 Flash page on Vercel and click Playground [1]. Sign in with your own account.
Before you send anything, look at the Providers table. You want MiniMax with the green Free tag. That's the only route the promotion covers [2].
My real playground run on 20 October, cropped. Top: the answer. Bottom: the provider row marked Free.
Now, in the app, open Show the exact generation prompt and copy it into the playground. It asks for three reports as JSON:
- Paid totals by category.
- Paid order count and total by region.
- Top two paid categories.
Here's one query MiniMax sent back:
SELECT category, SUM(total_cents) AS total_cents
FROM orders
WHERE status = 'paid'
GROUP BY category
ORDER BY category ASC
Paste one query at a time into the app's SQL box. Only the SQL, without the JSON quotes around it. Hit Run report locally. Done. ✅
The Gateway log reported 362 input tokens and 338 output tokens for that one answer, including 226 reasoning tokens. These are provider-reported usage counters, not an independent tokenizer measurement.
⚠️ Common mistake: assuming "free" means free on every route. Vercel can send a request to other providers, and the promotion only covers MiniMax serving it [2]. In code, pin the provider with
only. Theordersetting is just a preference and can fall back to someone else [3].
Step 3: Try to break it
This is the fun part. Click Try a write query. The box fills with DELETE FROM orders. Run it.
Original meme on my own screenshot. The rejection is real.
How does it say no? SQLite has a lesser-known feature called an authorizer [4]. It's a function SQLite calls for every action a query wants to do. You answer "allowed" or "denied". Mine allows reading the orders table plus a handful of math functions, and nothing else:
functions = {'sum', 'count', 'avg', 'min', 'max', 'round', 'coalesce'}
def authorize(action, first, second, *_):
if action == sqlite3.SQLITE_SELECT:
return sqlite3.SQLITE_OK
if action == sqlite3.SQLITE_READ and first == 'orders':
return sqlite3.SQLITE_OK
if action == sqlite3.SQLITE_FUNCTION and (second or '').lower() in functions:
return sqlite3.SQLITE_OK
return sqlite3.SQLITE_DENY
db.set_authorizer(authorize)
On top of that: SQLite's query-only mode is on, the database is rebuilt fresh in memory every run, only one statement runs at a time, queries over 8,000 characters are refused, queries exceeding an instruction budget get stopped, and you see at most 100 rows.
I threw six bad queries at it: DELETE, DROP TABLE, a PRAGMA, reading SQLite's internal schema table, a function that isn't on the list, and two statements glued together. All six were rejected. 🛑
Original meme. Emoji: Twemoji, CC-BY 4.0.
⚠️ Common mistake: checking "does the text start with SELECT?" and calling it safe. It blocks good queries and misses bad ones. This perfectly harmless query starts with
WITH:
WITH paid AS (SELECT region FROM orders WHERE status = 'paid') SELECT COUNT(*) AS paid_orders FROM paidThe authorizer looks at what the query does, so it allowed this one and returned 9. A text check would have thrown it out.
Did MiniMax get it right?
All three queries in the one saved model response matched this dataset. I wrote the expected answers before asking the model, then checked each query against those answers and against a separate Python calculation.
- By category: Books 4300, Games 9000, Tools 10500 (in cents).
- By region: North 3 paid orders, South 2, West 4.
- Top two: Tools, then Games.
The traps it avoided: it filtered to paid orders only, and it sorted the way I asked. Those are two easy mistakes a "looks right" query can hide.
Fine print
- This is one model answer with three queries, on twelve fake rows. Nice demo, not a model ranking.
- The guardrails fit this little app. A real database with real users needs proper permissions, timeouts and a separate read-only account, not just this callback.
- The free window is temporary. Check the Providers table before you send anything.
The pattern is the takeaway: model writes, database decides, checker confirms. Try it with your own synthetic schema and questions: a starting point for a local report tool. The model route is free for a couple of days, at least. 😉
References
- Vercel AI Gateway: MiniMax M3.1 Flash model page and playground.
- Vercel changelog: MiniMax M3.1 Flash free on AI Gateway.
- Vercel docs: provider filtering and ordering.
- Python 3.14 docs: sqlite3 authorizers, progress handlers and execute.
Asset credits: screenshots are my own captures of the app and the Vercel playground. Memes are original. Emoji graphics: Twemoji, CC-BY 4.0.
AI disclosure
AI was used to draft the article.




Top comments (0)