<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sanghyeon Han</title>
    <description>The latest articles on DEV Community by Sanghyeon Han (@shankr).</description>
    <link>https://dev.to/shankr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4219722%2F93f1a961-a698-43ad-bcbd-c68105a37d77.png</url>
      <title>DEV Community: Sanghyeon Han</title>
      <link>https://dev.to/shankr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shankr"/>
    <language>en</language>
    <item>
      <title>First-ever unforgettable hackathon</title>
      <dc:creator>Sanghyeon Han</dc:creator>
      <pubDate>Sat, 31 Oct 2026 23:17:20 +0000</pubDate>
      <link>https://dev.to/shankr/first-ever-unforgettable-hackathon-127o</link>
      <guid>https://dev.to/shankr/first-ever-unforgettable-hackathon-127o</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/mlh-hackathon"&gt;MLH x DEV Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My teammate Bibash Chaudhary and I built &lt;strong&gt;&lt;a href="https://mathwise.ink" rel="noopener noreferrer"&gt;MathWise&lt;/a&gt;&lt;/strong&gt; in 24 hours at HackGB 2026. You write your own Precalc, Calc I or Calc II solution by hand with a stylus, one step per line, and MathWise finds the &lt;strong&gt;first&lt;/strong&gt; line that went wrong. Then it gives you only as much help as you asked for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why we built it
&lt;/h3&gt;

&lt;p&gt;We're both in Calc II, and like a lot of students we check homework by pasting it into a chatbot. That works until it doesn't. When we gave Gemini my handwritten solution to&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop op-symbol large-op"&gt;∫&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mop"&gt;&lt;span class="mop"&gt;sin&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;it got the final answer right, but it blamed a line that was actually correct. My real mistake was somewhere else. If I had trusted it, I would have "fixed" the part that was fine.&lt;/p&gt;

&lt;p&gt;The other problem is that a chatbot just hands you the full solution. At midnight before a quiz I don't want the answer. I want to know &lt;em&gt;where&lt;/em&gt; I went wrong, and sometimes only &lt;em&gt;whether&lt;/em&gt; I went wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write on a ruled canvas&lt;/strong&gt; in the browser. Pen and mouse draw; touch is ignored, so you can rest your palm on the screen like on paper. Bibash built this part, and it works on both Android and iOS with no native app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a help level&lt;/strong&gt; before you check (the five levels are in the table below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Press Check.&lt;/strong&gt; Gemini reads each line &lt;em&gt;exactly as written&lt;/em&gt; and then finds the first wrong line. Lines that follow correctly from an earlier mistake aren't flagged, like "error carried forward" credit on an exam.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SymPy checks the final answer with math, not AI.&lt;/strong&gt; If your antiderivative is wrong, it differentiates it and shows you an x where it doesn't match the integrand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stuck? Take a hint.&lt;/strong&gt; Each hint steps down one level (Check → Nudge → Pinpoint → Walkthrough). Hints make &lt;strong&gt;no new AI call&lt;/strong&gt;: the server keeps the one analysis it already has, and plain code decides which fields each level may show. If you're still stuck at Walkthrough, you can reveal a full worked solution after a confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every mistake links to the matching section&lt;/strong&gt; of a free OpenStax textbook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The five help levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Walkthrough&lt;/td&gt;
&lt;td&gt;Which line is wrong, why, and how to fix it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Pinpoint&lt;/td&gt;
&lt;td&gt;Which line is wrong and why; you fix it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Nudge&lt;/td&gt;
&lt;td&gt;One guiding question, no verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Check&lt;/td&gt;
&lt;td&gt;Only correct or incorrect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Exam&lt;/td&gt;
&lt;td&gt;Nothing until you submit; then a full review, and the problem locks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I did the backend: the help levels, the Gemini prompts and calls, the SymPy checker and the Modal deployment. Bibash did the whole frontend: the canvas with palm rejection, the tools, the help-level dial, History and themes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mocked tests aren't enough with an AI API.&lt;/strong&gt; We ended with 106 backend tests, all with Gemini mocked, and the very first real call failed with a bug none of them could see (more on that below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading handwriting and judging math are two different problems.&lt;/strong&gt; Splitting them into two calls made each one easier to test and to fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI is good at explaining and bad at being sure.&lt;/strong&gt; Anything that has to be right (what a help level reveals, model fallbacks, whether the final answer is correct) ended up as ordinary code or real math, not a prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/wfEeF7ksBN4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://mathwise.ink" rel="noopener noreferrer"&gt;https://mathwise.ink&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup URL&lt;/strong&gt; (if your network blocks the new domain): &lt;a href="https://shan119660--mathwise-web.us-east.modal.run" rel="noopener noreferrer"&gt;https://shan119660--mathwise-web.us-east.modal.run&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No stylus?&lt;/strong&gt; Add &lt;code&gt;?mock&lt;/code&gt; to the URL for a canned example with no AI calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first check after the app has been idle takes a few extra seconds while the server starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Walkthrough
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxf7mnilxjx04zq4fwwpq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxf7mnilxjx04zq4fwwpq.png" alt="MathWise cover: handwrite your solution and choose how much help you get" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You pick a help level on the dial before checking. Hints step down from there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuhka4ujvim4cmkwpq2ep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuhka4ujvim4cmkwpq2ep.png" alt="The five help levels, from Walkthrough to Exam" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's an integration by parts problem, ∫x sin x dx. Each handwritten line shows the LaTeX that Gemini read next to it, mistake included. Line 2 says &lt;code&gt;v = cos x&lt;/code&gt;, which is wrong (the antiderivative of sin x is −cos x), and that's the line that lights up. Lines 3 and 4 follow correctly from it, so they aren't flagged.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdu03hl7lebcy6nfhuiq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdu03hl7lebcy6nfhuiq4.png" alt="Four handwritten lines with their transcriptions; line 2, v = cos x, is highlighted" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At Walkthrough you get the why and the fix. Underneath, SymPy has checked the final answer on its own: "The derivative of your final answer is not the integrand: at x = −2.737 they differ: −1.0774 vs 1.0774." There's also a link to OpenStax &lt;em&gt;Calculus Volume 2&lt;/em&gt;, section 3.1 Integration by Parts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdof7ts4havv0s811hode.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdof7ts4havv0s811hode.png" alt="Walkthrough feedback with the SymPy counterexample and the OpenStax link" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Partner Technologies
&lt;/h2&gt;

&lt;p&gt;We used two MLH partner technologies: the &lt;strong&gt;Gemini API&lt;/strong&gt; and a &lt;strong&gt;GoDaddy Registry&lt;/strong&gt; domain. Gemini is the core of the app, so most of this section is about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini API
&lt;/h3&gt;

&lt;p&gt;We call Gemini through the &lt;code&gt;google-genai&lt;/code&gt; Python SDK. The primary model is &lt;code&gt;gemini-3.8-flash&lt;/code&gt;, and a normal check makes &lt;strong&gt;two separate calls&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Call 1: transcription, literally
&lt;/h4&gt;

&lt;p&gt;The canvas crops each ruled row into its own image. The backend sends all rows in one request, each image preceded by a text label (&lt;code&gt;[0]&lt;/code&gt;, &lt;code&gt;[1]&lt;/code&gt;, ...), so the model can't lose track of which line is which.&lt;/p&gt;

&lt;p&gt;The most important line in the transcription prompt is this one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Transcribe EXACTLY what is written. NEVER fix mistakes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vision models love to "help." If a student writes &lt;code&gt;v = cos x&lt;/code&gt; where it should be &lt;code&gt;−cos x&lt;/code&gt;, a model that reads for meaning will quietly write the correct version, and the mistake we're supposed to find disappears before the checker ever sees it. So the prompt tells Gemini to keep every sign, constant and exponent as written, and calls out the things it tends to smooth over: minus signs, &lt;code&gt;+ C&lt;/code&gt;, what's inside an exponent (&lt;code&gt;e^{x^2/2 + C}&lt;/code&gt; vs &lt;code&gt;e^{x^2/2} + C&lt;/code&gt;), &lt;code&gt;dx&lt;/code&gt; and parentheses. Each row also gets an &lt;code&gt;unclear&lt;/code&gt; flag when the model isn't confident about a symbol, so the app can tell you instead of guessing silently.&lt;/p&gt;

&lt;h4&gt;
  
  
  Call 2: analysis, as structured output
&lt;/h4&gt;

&lt;p&gt;The second call gets only text: the problem plus the transcribed steps, numbered. It returns structured JSON, and I let the SDK enforce the shape by passing a Pydantic model as the &lt;code&gt;response_schema&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Analysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incorrect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;wrong_step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StrictInt&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;mistake_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MistakeType&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;   &lt;span class="c1"&gt;# sign_error, chain_rule, ibp_setup, ...
&lt;/span&gt;    &lt;span class="n"&gt;why&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;hint_question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="c1"&gt;# The problem and the final answer in SymPy syntax, for the math check
&lt;/span&gt;    &lt;span class="n"&gt;cas_kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CasKind&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;cas_expr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;cas_lower&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;cas_upper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;cas_answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GenerateContentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;response_mime_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;response_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Analysis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_instruction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Structured output is what makes the help levels possible. Gemini analyzes the solution &lt;strong&gt;once&lt;/strong&gt;, the server stores the whole &lt;code&gt;Analysis&lt;/code&gt; for up to an hour, and a plain lookup table decides which fields each level may see. Level 4 only gets &lt;code&gt;verdict&lt;/code&gt;. Level 3 only gets &lt;code&gt;hint_question&lt;/code&gt;. A hint just moves you one row down the table. That's why the levels are consistent and hints cost nothing: asking the model five different ways would have given five slightly different stories.&lt;/p&gt;

&lt;p&gt;I still don't fully trust the schema, so the backend validates the response again. A &lt;code&gt;wrong_step&lt;/code&gt; outside the submitted lines, or a transcription with the wrong number of rows or out-of-order indexes, counts as unusable and gets one retry.&lt;/p&gt;

&lt;p&gt;A few prompt rules came from real failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;why&lt;/code&gt; must never contain the fix&lt;/strong&gt;, because Pinpoint shows &lt;code&gt;why&lt;/code&gt; but hides &lt;code&gt;fix&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The guiding question must look the same whether the solution is right or wrong.&lt;/strong&gt; Otherwise Nudge would leak the verdict through its tone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No line numbers in the feedback text.&lt;/strong&gt; Gemini numbers rows from 0, the app shows them from 1, so "line 3" in the text would point at the wrong line. Now the prompt says to quote the math instead ("where you wrote sin³/3") and the app does the highlighting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy the final answer into SymPy syntax exactly as written, never corrected.&lt;/strong&gt; That's the &lt;code&gt;cas_*&lt;/code&gt; fields. If Gemini fixed the answer while translating it, the math check would verify the wrong thing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Bug 1: every live call failed with a 400
&lt;/h4&gt;

&lt;p&gt;All my request models used &lt;code&gt;ConfigDict(extra="forbid")&lt;/code&gt;, which is a good habit for an API. So I added it to the response models too. When you pass such a model as &lt;code&gt;response_schema&lt;/code&gt;, the generated JSON schema includes &lt;code&gt;additionalProperties: false&lt;/code&gt;, and Gemini's &lt;code&gt;response_schema&lt;/code&gt; rejects it with a 400. Every single live call failed.&lt;/p&gt;

&lt;p&gt;My tests mocked Gemini, so all of them passed. Only a real call found it. The fix was one line, plus a docstring on &lt;code&gt;Analysis&lt;/code&gt; so nobody puts it back:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No extra="forbid" on models used as Gemini response schemas: it adds additionalProperties, which Gemini's response_schema rejects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Bug 2: one check took over 3 minutes
&lt;/h4&gt;

&lt;p&gt;During the event Gemini was busy, and we started getting &lt;code&gt;503&lt;/code&gt; "high demand" and &lt;code&gt;429&lt;/code&gt; rate-limit errors. Worse, one check took &lt;strong&gt;over 3 minutes&lt;/strong&gt;. The app wasn't stuck: the SDK was quietly retrying up to 5 times with backoff on 503 and 429, and we couldn't see any of it.&lt;/p&gt;

&lt;p&gt;The fix had three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Turn off the SDK's retries&lt;/strong&gt;, so a failure comes back right away:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http_options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout_s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="n"&gt;retry_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpRetryOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
   &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A time limit per model&lt;/strong&gt;: 20 seconds each for reading and for checking. In normal times reading takes about 4 s and checking about 5 s, so 20 s means something is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fall back across Gemini models&lt;/strong&gt; instead of retrying the same overloaded one. If a model returns a 5xx, a 408 or a 429, or times out, &lt;code&gt;generate()&lt;/code&gt; moves to the next one:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="n"&gt;CHECK_FALLBACKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3.7-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3.6-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3.5-flash-lite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="n"&gt;TRANSCRIBE_FALLBACKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3.5-flash-lite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters. Flash-Lite is last for checking, because in an early test it blamed the wrong row (it missed a quotient-rule error). For transcription it read rows the same as Flash, so it's a fine fallback there. Each model also had its own free-tier quota, so trying more full Flash models first kept answers accurate when one model was rate limited.&lt;/p&gt;

&lt;p&gt;After this, a slow check finishes in seconds instead of minutes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Bug 3: reading six lines took 41 seconds
&lt;/h4&gt;

&lt;p&gt;Transcription at the model's default thinking level took about &lt;strong&gt;41 s&lt;/strong&gt; for six rows. Reading handwriting doesn't need much reasoning, so I set the thinking level for transcription only to &lt;code&gt;"low"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;TRANSCRIBE_THINKING_LEVEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GEMINI_TRANSCRIBE_THINKING_LEVEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That brought it down to &lt;strong&gt;about 4 s&lt;/strong&gt;, with the same output. Checking keeps the model's default thinking, because finding the wrong step is the part that actually needs it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Not trusting the AI: SymPy
&lt;/h4&gt;

&lt;p&gt;Even a strong model doesn't always find the first wrong line. Benchmarks like ProcessBench show that, and so did our own sin³x story. So Gemini's last job is translation, not judgment: it writes the problem and the final answer in SymPy syntax, and plain Python decides if the answer is right. It differentiates an antiderivative and compares it with the integrand at random points, integrates definite integrals numerically, takes two-sided limits, substitutes solutions back in (and checks none is missing), and plugs solutions into differential equations.&lt;/p&gt;

&lt;p&gt;Because that SymPy text comes from an AI, and SymPy's parser evaluates Python, the parser only accepts a whitelist of math names and plain operators. It rejects huge constants like &lt;code&gt;9**9**9&lt;/code&gt; before evaluating them, and every check stops after 12 seconds. SymPy checks the &lt;strong&gt;final answer only&lt;/strong&gt;. It can't tell you which line is wrong, and a right final answer can still hide a wrong step.&lt;/p&gt;

&lt;p&gt;The optional full worked solution is a third Gemini call, and SymPy checks its final answer too. If the check fails, we return "not available" instead of a guess.&lt;/p&gt;

&lt;h4&gt;
  
  
  What working with Gemini was like
&lt;/h4&gt;

&lt;p&gt;Structured output was the best part. Getting typed, validated JSON straight into a Pydantic model meant the rest of the backend could be normal, testable code. The hard part was everything around reliability during a busy event: overload, rate limits, silent retries and latency. None of it shows up in mocked tests.&lt;/p&gt;

&lt;p&gt;We also had a practical constraint. The cloud credits we were promised didn't work, so we built the whole thing on the Gemini &lt;strong&gt;free tier&lt;/strong&gt;, which is part of why falling back across models mattered so much. For the final app we moved to a small paid key. Under the Gemini API terms, Google doesn't use paid-tier data to improve its products, which matters when the input is students' handwriting.&lt;/p&gt;

&lt;p&gt;Repeat checks of the same work are cached (a SHA-256 of the problem, steps and images), so they come back instantly, give the same answer and use no quota.&lt;/p&gt;

&lt;h3&gt;
  
  
  GoDaddy Registry: mathwise.ink
&lt;/h3&gt;

&lt;p&gt;Our domain is &lt;strong&gt;&lt;a href="https://mathwise.ink" rel="noopener noreferrer"&gt;mathwise.ink&lt;/a&gt;&lt;/strong&gt;. We picked &lt;code&gt;.ink&lt;/code&gt; because MathWise is stylus-first: you write your work in ink. It's also short enough to say out loud at the end of a demo.&lt;/p&gt;

&lt;p&gt;We pointed it at the app on Modal. The surprise came at the venue: the campus Wi-Fi blocked the new domain, and it still doesn't load on campus Wi-Fi to this day. It works fine everywhere else. So at the event we used the Modal URL directly and shared it with a QR code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other tech (not MLH partners)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modal&lt;/strong&gt; runs the FastAPI backend and the built React frontend as one HTTPS web app. It's pinned to &lt;strong&gt;one container&lt;/strong&gt; on purpose: hints read the stored analysis from memory, and containers don't share memory. It stays up 20 minutes after the last request, then scales to zero. The Gemini key is a Modal secret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React, TypeScript, Vite, Tailwind CSS, KaTeX for math.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI, Pydantic, SymPy, mpmath, pytest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI coding assistants:&lt;/strong&gt; we used Codex and Claude Code to help write and review code. Gemini is the only AI in the running app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hackathon Experience
&lt;/h2&gt;

&lt;p&gt;HackGB 2026 was the &lt;strong&gt;first-ever HackGB&lt;/strong&gt;, a 24-hour hackathon at UW-Green Bay. It ran from Saturday, October 17 at 12 PM to Sunday, October 18 at 12 PM, mostly in the Brown County STEM Innovation Center. It was also &lt;strong&gt;our first hackathon&lt;/strong&gt;: Bibash and I were a team of two.&lt;/p&gt;

&lt;p&gt;We split the work cleanly: Bibash on the frontend, me on the backend, with the API contract written down in a shared doc so we could build in parallel. That worked better than I expected. The stressful parts were all on the infrastructure side: free-tier quotas, Gemini overloading while lots of teams hit the same models, and a domain the campus Wi-Fi wouldn't load.&lt;/p&gt;

&lt;p&gt;Since it was my first hackathon, I honestly didn't know what to expect. I was curious what everyone else was building, but what struck me most was how hard every team in the room was pushing toward the same goal: winning. We came to win too, so we gave it everything we had. I remember it being past 2 AM, my head foggy, looking around at other teams still coding through the night, and knowing we couldn't give up either.&lt;/p&gt;

&lt;p&gt;The other teams were amazing. Some formed their team right there at the event and still got in sync, which impressed me a lot. I also got to swap stories with students from other schools about their college life and their past hackathons.&lt;/p&gt;

&lt;p&gt;The mentors were just as impressive. We'd worried that our idea was too cliché, but they showed us how to make use of exactly that and where to improve it. I already knew some of the organizers, and spending a whole weekend at their event let me get to know them better. It was a place to learn a lot.&lt;/p&gt;

&lt;p&gt;The sponsors were a highlight too. I talked with ATS, FTI, Bay Tek, MLH and others, and learned something from each of them. Meeting Bay Tek made me really happy, because I've always been into arcades. And ATS turned out to be where a friend I'd met through a completely different path works, so that unexpected connection was a nice surprise.&lt;/p&gt;

&lt;p&gt;What I'll remember most is the building itself. The idea came from Bibash, my friend and teammate, and it could easily have stayed just another familiar idea. I turned it into a system, we each took our own part, and we kept pushing it further. That process was fun and intense, and it's the part I'll keep.&lt;/p&gt;

&lt;p&gt;We entered the Education track and Best Beginner Hack, Best UI/UX Hack, Best Use of Gemini API, Best Use of Modal and Best Domain Name from GoDaddy Registry. Honestly, part of me still wishes we'd taken first place overall. But we won &lt;strong&gt;Best Education Hack&lt;/strong&gt;, and I'm really happy about it. For a first hackathon I would have been glad to leave with just the experience, so winning a prize on top of that means a lot.&lt;/p&gt;

&lt;p&gt;My biggest takeaway: in 24 hours, the AI part was the easy part to get working and the hard part to trust. Most of what makes MathWise useful is the plain code around the model: the level table, the fallbacks, the literal transcription, and SymPy saying "here's an x where you're wrong."&lt;/p&gt;

&lt;p&gt;This was my first hackathon, but definitely not my last. I'm already looking forward to the next one.&lt;/p&gt;

&lt;p&gt;Try it at &lt;strong&gt;&lt;a href="https://mathwise.ink" rel="noopener noreferrer"&gt;mathwise.ink&lt;/a&gt;&lt;/strong&gt;. Ink, because you write your work in ink.&lt;/p&gt;

</description>
      <category>hackathon</category>
      <category>education</category>
      <category>mlhacks</category>
      <category>devchallenge</category>
    </item>
  </channel>
</rss>
