It was 11 PM, two days before my machine learning midterm. I had sixty pages of lecture notes and the attention span of a goldfish. So I did what any reasonable student would do: I built a bot to turn the notes into flashcards.
The plan sounded innocent. Feed the notes to a free model. Ask for JSON. Serve the cards from a free server. Quiz myself on my phone. No paid APIs, no GPU, no excuses.
Then I counted the lies. 37 out of 400 flashcards were wrong. Not subtly wrong — embarrassingly wrong. One card defined precision as “the fraction of true positives among all actual positives.” That’s recall. The model had swapped two terms I needed for the exam.
This is the story of that project, the 40-line filter I wrote to catch the errors, and what a free model taught me about trust.
The setup
My budget for this project was exactly zero dollars. MonkeyCode’s free tier gave me two things I needed: free model access and a free server. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The model endpoint accepts plain HTTP requests. The server runs a small Python app. That’s the whole stack — no orchestration, no vector database, no Kubernetes. Just a script, a JSON file, and a web page.
You need Python 3.10+, the requests library, and an account with a free model endpoint. The goal: generate 400 flashcards from my lecture notes, each with a term, a definition, and an example sentence. Then serve them as a flip-card page on my phone.
The naive implementation
I wrote a script that chunks the notes, sends each chunk to the model, and collects the JSON responses.
import json
import requests
NOTES = open('notes.txt').read()
CHUNK = 8000 # characters, not tokens. Naive, I know.
def generate_cards(chunk):
prompt = f'''You are a flashcard generator. From the notes below,
produce exactly 10 flashcards. Return ONLY JSON, no markdown.
Format: [{{"term": "...", "definition": "...", "example": "..."}}]
Notes:
{chunk}'''
r = requests.post(
'https://<your-free-model-endpoint>/v1/chat/completions',
json={
'model': 'free-model',
'messages': [{'role': 'user', 'content': prompt}],
},
timeout=60,
)
content = r.json()['choices'][0]['message']['content']
return json.loads(content)
cards = []
for i in range(0, len(NOTES), CHUNK):
cards.extend(generate_cards(NOTES[i:i + CHUNK]))
json.dump(cards, open('flashcards.json', 'w'), indent=2)
print(f'Generated {len(cards)} cards')
It worked. Too well, in a way. The script produced 400 cards in about twenty minutes, and every response parsed as valid JSON. I uploaded the file to my free server, added a tiny HTML page with a flip animation, and went to bed feeling smug.
The audit
The next morning I did a spot check. I opened the JSON and read twenty random cards.
Six were wrong.
That extrapolated to roughly 120 wrong cards out of 400. I did the math twice, hoping it would change. It didn’t. Reading all 400 by hand would take two hours I didn’t have, so I studied the failures instead. Three patterns emerged.
First, confusable pairs. The model swapped precision and recall, bias and variance, dropout and batch normalization. It knew the vocabulary; it mixed up the relationships.
Second, direction errors. A card about gradient descent said we use it to “maximize the loss function.” The words were right. The sign was wrong.
Third, empty examples. Some cards had example sentences that never mentioned the term. A card about “learning rate” came with an example about “the model converged quickly.” Technically true. Completely useless.
The filter
I wrote a 40-line filter that flags suspicious cards instead of deleting them. The goal was to shrink 400 cards down to a short review list.
import json
cards = json.load(open('flashcards.json'))
CONFUSABLES = {
'precision': ['recall'],
'recall': ['precision'],
'bias': ['variance'],
'variance': ['bias'],
'dropout': ['batch normalization'],
'batch normalization': ['dropout'],
}
def check(card):
term = card.get('term', '').lower()
definition = card.get('definition', '').lower()
example = card.get('example', '').lower()
for key in ('term', 'definition', 'example'):
if len(card.get(key, '')) < 5:
return f"'{key}' missing or too short"
for other in CONFUSABLES.get(term, []):
if other in definition and term not in definition:
return f"definition looks like it's about '{other}'"
if term not in example:
return "example doesn't mention the term"
if 'maximize' in definition and 'loss' in definition:
return 'check direction: maximizing loss is usually wrong'
return None
flagged = []
for card in cards:
reason = check(card)
if reason:
flagged.append((reason, card))
print(f'Flagged {len(flagged)} of {len(cards)} cards')
for reason, card in flagged:
print(f"- {card['term']}: {reason}")
The filter flagged 37 cards. I reviewed those 37 by hand in about twenty minutes. Thirty-one were genuinely wrong. Six were false positives — a card about bias, for example, legitimately mentioned variance because of the bias-variance tradeoff.
Thirty-one wrong cards out of 400 is a 7.75% error rate. The filter didn’t catch everything, but it compressed two hours of reading into twenty minutes of judgment. That’s the real win.
What the free tier taught me
The free model and the free server changed how I thought about the project.
Because tokens were free, I stopped optimizing my prompts and started optimizing my verification. I made forty API calls without flinching. The whole project sipped about one percent of the 10-million-token allowance. The constraint wasn’t cost — it was trust.
Because the server was free, I left the quiz page running for my study group. That had a side effect: the server went idle between sessions, and the first request after a break took a few seconds to wake up. A tiny cron ping fixed it. Free infrastructure has a rhythm you have to learn.
The deeper lesson is uncomfortable. The model’s job was to rephrase my notes, not to invent facts. It still swapped definitions. If a free model can’t reliably copy from a source that’s right there in the prompt, then any task that depends on accuracy needs a verification layer. The filter wasn’t a nice-to-have. It was the actual project.
Who shouldn’t use this approach
Let me be honest about the limits.
If you’re building something for customers, a free model tier and a free server come with availability and data-handling caveats. A server that sleeps is fine for my study group; it’s not fine for a product. A token allowance that resets is fine for a semester project; it’s not a business plan.
If your flashcards were for a medical or legal exam, a 7.75% error rate is disqualifying. You’d need a stronger model, retrieval over verified sources, or mandatory human review. Probably all three.
And if you need guaranteed uptime, don’t build on free infrastructure. Build on free infrastructure to learn, to prototype, and to fail cheaply.
What you should understand now
Free models are excellent at drafting and terrible at being trusted. Validation is the product. The filter is the project.
A model that rephrases your own notes will still lie. So the question isn’t whether the output is wrong — it’s whether you can find the wrong parts before they reach someone who believes them.
Your turn
Here’s an extension exercise. Add a next_review date to each card and build a tiny spaced-repetition scheduler into the HTML page. Then write a second filter that flags near-duplicate definitions using difflib.SequenceMatcher. You’ll learn more from that filter than from the flashcards themselves.
If you want to try this setup, MonkeyCode’s free tier is a reasonable place to start. But bring your own filter. The model will lie to you — the question is whether you’ll catch it.
Top comments (0)