<?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: Sangyoon Lee</title>
    <description>The latest articles on DEV Community by Sangyoon Lee (@globalmatchhub_b7ad9cba).</description>
    <link>https://dev.to/globalmatchhub_b7ad9cba</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%2F4133533%2F792bf7a7-4835-4751-8944-848601fe60e3.png</url>
      <title>DEV Community: Sangyoon Lee</title>
      <link>https://dev.to/globalmatchhub_b7ad9cba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/globalmatchhub_b7ad9cba"/>
    <language>en</language>
    <item>
      <title>The client asked for "one more thing" again. So I built ScopeSloth to answer.</title>
      <dc:creator>Sangyoon Lee</dc:creator>
      <pubDate>Tue, 20 Oct 2026 06:54:27 +0000</pubDate>
      <link>https://dev.to/globalmatchhub_b7ad9cba/the-client-asked-for-one-more-thing-again-so-i-built-scopesloth-to-answer-211k</link>
      <guid>https://dev.to/globalmatchhub_b7ad9cba/the-client-asked-for-one-more-thing-again-so-i-built-scopesloth-to-answer-211k</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;ScopeSloth&lt;/strong&gt; answers the client message you dread: the one that says "looks great, just a few small things" and then lists five.&lt;/p&gt;

&lt;p&gt;You paste what you agreed (scope, rate, revision rounds) and the client's latest message. An agent lists every separate request. A second agent searches your agreement for each one and cites the line that covers it. Then plain code, not the model, decides the money: hours times the rate in your agreement, and whether a design tweak uses up one of your included revision rounds. A third agent drafts a calm reply: what is covered, what is an add-on and its exact price, and one question for anything unclear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it is for:&lt;/strong&gt; freelancers who have a written scope and still end up doing extra work for free, because arguing about it feels worse than doing it. The lazy move here is not to skip the work. It is to not have the argument.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://scopesloth.sellerkit.workers.dev" rel="noopener noreferrer"&gt;https://scopesloth.sellerkit.workers.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Load a sample&lt;/strong&gt; (a made-up bakery website and a made-up client message), then &lt;strong&gt;Sort the requests&lt;/strong&gt;. A run took 5 to 10 seconds in my tests.&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%2Fiyifa3ks0qvk4fvk5mhk.jpg" 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%2Fiyifa3ks0qvk4fvk5mhk.jpg" alt="Five requests sorted: two in scope, three extra work with prices" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The header photo and logo tweaks are revisions, so they are free and use one of the two included rounds. The ordering page, Instagram feed and PDF menu are not in the agreement, so they get a price.&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%2Fo86alb6zlbrzgiuagn9z.jpg" 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%2Fo86alb6zlbrzgiuagn9z.jpg" alt="The drafted reply, with the check result underneath" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reply only contains amounts the code computed. If the model invents &lt;code&gt;$400&lt;/code&gt;, the draft goes back with the exact problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/globalmatchhub/scopesloth" rel="noopener noreferrer"&gt;https://gitlab.com/globalmatchhub/scopesloth&lt;/a&gt; (MIT)&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; a &lt;a href="https://mastra.ai" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt; workflow on Cloudflare Workers, calling open-weight models on Workers AI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; &lt;code&gt;gpt-oss-20b&lt;/code&gt; (Apache 2.0) is the default, reasoning effort low. &lt;code&gt;Llama 4 Scout 17B&lt;/code&gt; is the second choice and the backup writer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow:&lt;/strong&gt; &lt;code&gt;createWorkflow(...).then(listRequests).then(checkScope).then(writeReply)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One tool:&lt;/strong&gt; the checker agent has &lt;code&gt;search_scope&lt;/code&gt;. It takes several queries at once and returns numbered agreement lines (&lt;code&gt;S3&lt;/code&gt;). A cited line only counts if &lt;code&gt;search_scope&lt;/code&gt; really returned it in this run. A request marked "in scope" with no valid citation becomes "unclear".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small models answer in lines, not JSON.&lt;/strong&gt; &lt;code&gt;#2 extra - | new | 3 | not in the list&lt;/code&gt; is parsed by code, with a fallback when parsing fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three checks, one vote.&lt;/strong&gt; The checker runs three times in parallel. Code keeps the verdict most checks agree on and the median of their hours, and the page shows "2/3 checks agree" next to each request. A tie with no "in scope" vote counts as extra work; a tie that involves "in scope" stays unclear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rules are plain JavaScript&lt;/strong&gt; (&lt;code&gt;src/scope.js&lt;/code&gt; and &lt;code&gt;src/parse.js&lt;/code&gt;, 19 tests, no model):

&lt;ul&gt;
&lt;li&gt;rate and revision rounds are read from the agreement text (&lt;code&gt;$60/hour&lt;/code&gt;, &lt;code&gt;2 rounds of revisions&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;hours from the model are clamped to halves between 0.5 and 40, so one wild guess cannot become a huge line item&lt;/li&gt;
&lt;li&gt;price = hours x rate, always computed in code&lt;/li&gt;
&lt;li&gt;a change to delivered work uses a revision round, and becomes billable only when the rounds run out&lt;/li&gt;
&lt;li&gt;the reply may only contain amounts the code computed, and must price every extra item&lt;/li&gt;
&lt;li&gt;no dashes, placeholders or phrases like "as per our contract" or "unfortunately"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What broke on the way
&lt;/h3&gt;

&lt;p&gt;I ran the sample on the deployed Worker and read what came back.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What I changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Move the logo a bit to the left" was priced as $30 extra work. It is a design tweak, and two revision rounds were included.&lt;/td&gt;
&lt;td&gt;Code decides "change vs new" from the request's words and overrides the model. A tweak is free while rounds remain, and billed once they are used up (tested both ways).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The model labelled "add an Instagram feed" as a change to delivered work&lt;/td&gt;
&lt;td&gt;The same override: build words (add, feed, payment, PDF) force "new"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One run answered for only some of the requests, so the rest showed "unclear"&lt;/td&gt;
&lt;td&gt;The missing numbers are asked again, once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A request the agreement says nothing about came back "unclear"&lt;/td&gt;
&lt;td&gt;Silence in the agreement means extra work. Code turns it into extra if no line was cited and no line is close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft replies came with a non-breaking hyphen in "add-on"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;undash&lt;/code&gt; normalises it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One check often came back empty: the model put an arrow (&lt;code&gt;extra -&amp;gt; new&lt;/code&gt;) where the format has a pipe, copied from the "or -&amp;gt;" in my own format line, and the parser threw the line away&lt;/td&gt;
&lt;td&gt;The parser reads the arrow as a pipe (tested), and the format line no longer contains an arrow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same sample gave totals from $360 to $840 over 11 runs, because each run took one model guess per request&lt;/td&gt;
&lt;td&gt;The scope check now runs three times in parallel. Code keeps the majority verdict and the median hours, and shows "2/3 checks agree" next to each request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Honest numbers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;19 tests for the plain-code rules, run with &lt;code&gt;node --test&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deployed runs: 5 to 10 seconds, with slow outliers up to 32 s when Workers AI queued my request.&lt;/li&gt;
&lt;li&gt;About $0.0009 to $0.001 per run at Workers AI list prices (5 model calls: one list, three checks, one reply; around 3,300 tokens in, 1,000 out).&lt;/li&gt;
&lt;li&gt;Before the three-check vote, the same sample gave $360 to $840 over 11 runs. After it, 10 runs on a copy of the sample: 8 between $300 and $480, and 2 where the model called the ordering page or all three new requests "unclear" ($90 and $0), so the reply asked questions instead of quoting. The vote narrows the spread; it does not remove it. The hour estimates are still the model's guess, so the page tells you to adjust them. The money rules and the revision rule never varied.&lt;/li&gt;
&lt;li&gt;"Load a sample" followed by "Sort the requests" returns a stored answer, so everyone sees the same result for the made-up bakery. Only an answer where every request was decided by at least two of the three checks is stored. Text you paste yourself is never stored.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A client's message and a signed agreement are private. With an open-weight model on Workers AI the text is used for one request and not stored by my app, and anyone can read the code, run the same model on their own machine or swap it. The part that matters most is that the open pieces are swappable: the writing step has a backup, so if &lt;code&gt;gpt-oss-20b&lt;/code&gt; returns an empty draft, &lt;code&gt;Llama 4 Scout&lt;/code&gt; writes it instead, which is one line of config (that path did not trigger in my sample runs). A run costs a fraction of a cent, so a freelancer can use it on every message and never think about a subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;Not embedded. The code and the table of what broke are above.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;For the sample message, the by-hand version is reading the agreement again, deciding per request, estimating hours, doing the arithmetic, and writing a reply that is firm without sounding cold. ScopeSloth does that in under ten seconds, and the part that is most tempting to fudge, the arithmetic, is not left to a model. I have not measured this with real clients this week, so I am not going to claim a time saving beyond the sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Mastra&lt;/strong&gt;: a three-step Mastra workflow with a Mastra tool (&lt;code&gt;search_scope&lt;/code&gt;) and Mastra agents on open-weight models.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Code and English text were produced by an AI coding agent (Claude Code) working under my direction. I chose the topic, reviewed the results and published the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My last 38 cover letters all said something my resume didn't. So I built HonestFit.</title>
      <dc:creator>Sangyoon Lee</dc:creator>
      <pubDate>Fri, 16 Oct 2026 02:24:10 +0000</pubDate>
      <link>https://dev.to/globalmatchhub_b7ad9cba/my-last-38-cover-letters-all-said-something-my-resume-didnt-so-i-built-honestfit-3odl</link>
      <guid>https://dev.to/globalmatchhub_b7ad9cba/my-last-38-cover-letters-all-said-something-my-resume-didnt-so-i-built-honestfit-3odl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;HonestFit&lt;/strong&gt; writes a short cover letter that only says what your resume can back up.&lt;/p&gt;

&lt;p&gt;You paste your resume and a job posting. One agent reads the posting and lists the requirements. A second agent searches your resume for proof of each one and cites the exact line. A third writes the letter. Then plain code, not the model, checks every technology, every number and every "led / managed / architected" word in the letter against your resume. Anything it cannot find goes back to the model with the exact sentence, up to three drafts. A must-have you don't have gets one honest sentence instead of silence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for it:&lt;/strong&gt; people who apply for jobs in a second language. AI writing tools are very good at fluent English and not very good at staying inside the facts. When English isn't your first language, you can't always tell when a smooth sentence has quietly turned "used Docker" into "led platform migrations". A recruiter reads your letter and your resume side by side. If they don't match, the fluent letter hurts you.&lt;/p&gt;

&lt;p&gt;I would pay a few dollars a month for this. I know because I'm the first user (more below).&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://honestfit.sellerkit.workers.dev" rel="noopener noreferrer"&gt;https://honestfit.sellerkit.workers.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Load a sample&lt;/strong&gt; to fill in a made-up candidate and a made-up posting, then &lt;strong&gt;Check fit and write&lt;/strong&gt;. A run takes about 7 to 10 seconds.&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%2F0603cacza73mlobc33ke.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%2F0603cacza73mlobc33ke.png" alt="Fit table: each requirement with covered, partly or missing, and the resume line that proves it" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every requirement gets &lt;em&gt;covered&lt;/em&gt;, &lt;em&gt;partly&lt;/em&gt; or &lt;em&gt;missing&lt;/em&gt;, with the resume line (&lt;code&gt;L6&lt;/code&gt;) or your own note (&lt;code&gt;N1&lt;/code&gt;) as proof. The sample candidate has never used Kubernetes or Go, so those stay &lt;em&gt;missing&lt;/em&gt;. The model is not allowed to argue its way out of that.&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%2Fzfxsyacxtb8tc7vfnqnm.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%2Fzfxsyacxtb8tc7vfnqnm.png" alt="The letter, with the check result underneath" width="800" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The letter only uses what the table proved, and it admits the Kubernetes gap in one sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/globalmatchhub/honestfit" rel="noopener noreferrer"&gt;https://gitlab.com/globalmatchhub/honestfit&lt;/a&gt; (MIT)&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; a &lt;a href="https://mastra.ai" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt; workflow on Cloudflare Workers, calling open-weight models on Workers AI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; &lt;code&gt;gpt-oss-20b&lt;/code&gt; (Apache 2.0) is the default, with reasoning effort set to low. &lt;code&gt;Llama 4 Scout 17B&lt;/code&gt; is the second choice, and also the backup writer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow:&lt;/strong&gt; &lt;code&gt;createWorkflow(...).then(readPosting).then(matchEvidence).then(writeLetter)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One tool:&lt;/strong&gt; the matcher agent has &lt;code&gt;search_resume&lt;/code&gt;. It takes several queries at once and returns numbered resume lines. A cited line only counts if &lt;code&gt;search_resume&lt;/code&gt; actually returned it during this run, so the model can't cite a line it imagined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The checks are plain JavaScript&lt;/strong&gt; (about 200 lines, 18 tests), with no model involved:

&lt;ul&gt;
&lt;li&gt;Technology names, with aliases, so &lt;code&gt;k8s&lt;/code&gt; = Kubernetes and &lt;code&gt;Postgres&lt;/code&gt; = PostgreSQL.&lt;/li&gt;
&lt;li&gt;Numbers, compared by value: &lt;code&gt;120k&lt;/code&gt; = &lt;code&gt;120,000&lt;/code&gt;, &lt;code&gt;seven&lt;/code&gt; = &lt;code&gt;7&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Role words: led, managed, architected, head of and so on. They're only allowed if your resume already uses them.&lt;/li&gt;
&lt;li&gt;Em dashes, buzzwords and placeholders like &lt;code&gt;[Your Name]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Must-haves the letter ignores.&lt;/li&gt;
&lt;li&gt;A sentence like "I have not used Kubernetes yet" is treated as an honest gap, not a claim.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What broke on the way
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What I changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small models often broke the JSON schema for structured output&lt;/td&gt;
&lt;td&gt;Agents answer in a line format (&lt;code&gt;MUST: ...&lt;/code&gt;, `#3 covered L6 \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-oss sometimes returned an empty letter (it spent its tokens reasoning)&lt;/td&gt;
&lt;td&gt;{% raw %}&lt;code&gt;reasoning_effort: "low"&lt;/code&gt; and a bigger token budget. If a draft is still empty, Llama 4 Scout writes it instead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Node.js" split a sentence at the dot and caused false flags&lt;/td&gt;
&lt;td&gt;Sentences only split before a capital letter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"120,000 users" was flagged against "120k users"&lt;/td&gt;
&lt;td&gt;Numbers are compared by value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A draft said "I have led backend teams". Nothing caught it&lt;/td&gt;
&lt;td&gt;The role-word check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On my real resume, "SQL and NoSQL databases" came out &lt;em&gt;missing&lt;/em&gt;: the resume says MySQL, PostgreSQL and Redis, never "SQL". The letter then admitted a gap I don't have&lt;/td&gt;
&lt;td&gt;Category words now expand to the products resumes actually name (SQL to MySQL/PostgreSQL, NoSQL to Redis/MongoDB, cloud to AWS/GCP...)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Measured on the sample (October 15):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gpt-oss-20b: 7 to 9 seconds per letter, 4 to 5 model calls, about 3,000 input tokens and 800 output tokens. That is &lt;strong&gt;under $0.001 per letter&lt;/strong&gt; at Workers AI list price.&lt;/li&gt;
&lt;li&gt;Every gpt-oss letter admitted the Kubernetes gap.&lt;/li&gt;
&lt;li&gt;Some runs end with one flagged claim. The page shows it as a warning instead of hiding it.&lt;/li&gt;
&lt;li&gt;Llama 4 Scout took about 30 seconds and left more issues, so gpt-oss is the default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Known limit:&lt;/strong&gt; the checker knows technologies, numbers and role words. Adjectives still get through: one draft called a service "high-availability", and the resume never says that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The price makes the side hustle work.&lt;/strong&gt; Under a tenth of a cent per letter means $4 a month can cover heavy use with almost all of it as margin. At typical closed frontier-model prices, the same run with retries costs roughly 10 to 20 times more per letter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can swap models per step.&lt;/strong&gt; Reading and searching use gpt-oss. When it returns an empty draft, Llama 4 Scout writes it. That fallback is one line of code, because both are open-weight models behind the same interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model is not the judge.&lt;/strong&gt; Open weights let me choose small, cheap models on purpose and put the trust in code I can read and test. With a closed API I'd be paying more to trust the model more, which is the opposite of what this tool needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your resume could stay on your machine.&lt;/strong&gt; The same weights run locally. I haven't built that mode yet (my 2019 Intel laptop can't run gpt-oss-20b at a usable speed), so for now the page sends your text only for the length of one request and stores nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;Me.&lt;/p&gt;

&lt;p&gt;I'm a full-stack developer in Korea. I write English through AI translation, and I apply to remote jobs where I can work in writing. Before I built this, I had sent 38 cover letters. I ran HonestFit's checker over all 38 against my own resume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;38 of 38&lt;/strong&gt; said "13 years". My resume says "12 years 8 months".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;27&lt;/strong&gt; quoted numbers from side projects, like "90 of 94 MCP servers scanned" or "13 marketplace extensions", that don't appear anywhere on my resume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; named a technology my resume never mentions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it was a lie. But a recruiter holding both pages sees two slightly different people, and I had no idea until the checker showed me.&lt;/p&gt;

&lt;p&gt;Then I ran the full workflow with my real resume against a real Senior Backend posting from my job search (its requirements list). The first run marked "SQL and NoSQL databases" as missing and wrote that I had no database experience, which is wrong (that bug is in the table above). After the fix: 5 covered, 2 partly, 1 missing (working across time zones), one draft, 6.9 seconds, $0.0008.&lt;/p&gt;

&lt;p&gt;Did I pay? I'm the user, so no. But I would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Mastra&lt;/strong&gt;: the whole thing is a Mastra workflow with three agents and a tool, running on Cloudflare Workers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Code and English text were produced by an AI coding agent (Claude Code) working under my direction. The resume data, the 38 letters and the decision to publish are mine. I write in Korean.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Figma plugin UI has a null origin: how I read a Google Sheet without a server or API key</title>
      <dc:creator>Sangyoon Lee</dc:creator>
      <pubDate>Thu, 15 Oct 2026 02:27:40 +0000</pubDate>
      <link>https://dev.to/globalmatchhub_b7ad9cba/figma-plugin-ui-has-a-null-origin-how-i-read-a-google-sheet-without-a-server-or-api-key-idf</link>
      <guid>https://dev.to/globalmatchhub_b7ad9cba/figma-plugin-ui-has-a-null-origin-how-i-read-a-google-sheet-without-a-server-or-api-key-idf</guid>
      <description>&lt;p&gt;I was building a Figma plugin that pulls rows from a public Google Sheet. It looked easy: fetch the CSV, parse it, done. It did not work, and the reason is easy to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A Figma plugin UI runs in an iframe with a null origin. The Figma docs say this means the plugin can only call APIs that return Access-Control-Allow-Origin: *.&lt;/p&gt;

&lt;p&gt;I built a sandboxed iframe with a null origin and tested each way to read the sheet.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;docs.google.com gviz/tq?tqx=out:csv&lt;/td&gt;
&lt;td&gt;blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docs.google.com export?format=csv&lt;/td&gt;
&lt;td&gt;blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sheets.googleapis.com v4&lt;/td&gt;
&lt;td&gt;works, but needs an API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gviz/tq?tqx=out:json;responseHandler:cb loaded with a script tag&lt;/td&gt;
&lt;td&gt;works, no key, no server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why the export URL fails
&lt;/h2&gt;

&lt;p&gt;The final response of the export URL does have the allow header. But the first answer is a 307 redirect, and that redirect has no allow header. The browser checks the first one and stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked
&lt;/h2&gt;

&lt;p&gt;A script tag is not subject to CORS. So I ask Google for the JSON-with-callback format and load it as a script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* data.table.rows */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://docs.google.com/spreadsheets/d/&amp;lt;ID&amp;gt;/gviz/tq?tqx=out:json;responseHandler:cb&amp;amp;headers=0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add headers=0, otherwise Google guesses which row is the header and may eat your first data row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson
&lt;/h2&gt;

&lt;p&gt;Before writing any code that reads someone else's API from a browser extension or plugin, check with curl whether the API sends Access-Control-Allow-Origin: *. If there is a redirect, look at the headers of the first response, not the last one.&lt;/p&gt;

&lt;p&gt;The built-in browser pane of my tool could not run nested sandbox iframes, so I had to do this test in a real Chrome. Testing in the real environment saved me from shipping a plugin that fails on first load.&lt;/p&gt;

&lt;p&gt;Have you hit a null-origin problem in another plugin platform, and how did you get around it?&lt;/p&gt;




&lt;p&gt;I'm a full-stack developer, 13 years, based in Korea (UTC+9). I work in writing only, and use AI translation, so my English is simple. CV: &lt;a href="https://cv.namubase.com" rel="noopener noreferrer"&gt;https://cv.namubase.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>figma</category>
      <category>cors</category>
      <category>javascript</category>
      <category>googlesheets</category>
    </item>
    <item>
      <title>Available for remote contract work: 13-year full-stack dev (billing, admin, payments)</title>
      <dc:creator>Sangyoon Lee</dc:creator>
      <pubDate>Wed, 07 Oct 2026 00:10:56 +0000</pubDate>
      <link>https://dev.to/globalmatchhub_b7ad9cba/available-for-remote-contract-work-13-year-full-stack-dev-billing-admin-payments-3djf</link>
      <guid>https://dev.to/globalmatchhub_b7ad9cba/available-for-remote-contract-work-13-year-full-stack-dev-billing-admin-payments-3djf</guid>
      <description>&lt;p&gt;I'm Sangyoon Lee, a full-stack developer in Seoul with 13 years of experience, and I'm available for remote contract work starting now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note up front:&lt;/strong&gt; I don't speak English well. I communicate in writing and use AI translation for every message, so please expect written replies (within 24 hours, weekends included), not calls or video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I build and fix&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin dashboards and back-office tools (members, orders, settlement, permissions, change history)&lt;/li&gt;
&lt;li&gt;Billing, settlement and payment integrations (PG, recurring payments, kiosk/card terminals)&lt;/li&gt;
&lt;li&gt;Backends for web and mobile apps, plus Flutter apps shipped to iOS and Android&lt;/li&gt;
&lt;li&gt;Stack: Laravel and CodeIgniter, Spring Boot, Node.js/TypeScript, React, Vue, MySQL/PostgreSQL/Redis, AWS, Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Things you can check&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I replaced a production app's backend (serverless to Spring Boot WebFlux) without forcing a user app update.&lt;/li&gt;
&lt;li&gt;I built a multi-channel settlement reconciliation engine (Java 17, idempotent reprocessing, rollback).&lt;/li&gt;
&lt;li&gt;I shipped three new services in two months as the only developer, all on schedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How I work&lt;/strong&gt;&lt;br&gt;
Remote only. Scope and acceptance criteria are agreed in a document before I start, progress is written up every week, and I reply within 24 hours, weekends included. Everything stays in writing, so nothing gets lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instead of an interview, the first small task is free&lt;/strong&gt;&lt;br&gt;
We agree on the scope in writing first (up to about 10 hours of work), and I deliver within a week with a short written update every day. If you like the result, we continue on a paid basis. If not, you've lost nothing. One free task per company.&lt;/p&gt;

&lt;p&gt;If you have a backend, admin or integration task that keeps slipping, send me a short description and I'll reply with a plan.&lt;/p&gt;

&lt;p&gt;Portfolio: &lt;a href="https://cv.namubase.com" rel="noopener noreferrer"&gt;https://cv.namubase.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>freelance</category>
      <category>remote</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I ran my own SEO checker on my own sites. It called every Korean page "thin"</title>
      <dc:creator>Sangyoon Lee</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:57:54 +0000</pubDate>
      <link>https://dev.to/globalmatchhub_b7ad9cba/i-ran-my-own-seo-checker-on-my-own-sites-it-called-every-korean-page-thin-213b</link>
      <guid>https://dev.to/globalmatchhub_b7ad9cba/i-ran-my-own-seo-checker-on-my-own-sites-it-called-every-korean-page-thin-213b</guid>
      <description>&lt;p&gt;I built SearchLink, a small tool that checks a web page for SEO problems. A few weeks ago I ran it on my own 21 sites as a QA pass. All 21 answered with 200 OK, but the checker itself had three bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Korean pages were marked "not enough text".&lt;/strong&gt;&lt;br&gt;
It counted words by splitting on spaces. Korean, Chinese and Japanese don't work like that, so a full Korean page looked almost empty. Fix: for CJK text, count every 2 characters as one word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Title and description limits were English only.&lt;/strong&gt;&lt;br&gt;
Korean characters are wider, so they get cut off in search results earlier. I split the limits: 33 characters for titles and 85 for descriptions when the page is Korean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It nagged pages I hid on purpose.&lt;/strong&gt;&lt;br&gt;
If a page is noindex or blocked by robots.txt, the checker now just says "hidden on purpose" and stops scoring it.&lt;/p&gt;

&lt;p&gt;What I took from this: the best test data was my own real sites, in my own language. English samples would never have shown me bug #1. After the fixes, 110 tests on the paid build and 18 on the free build pass.&lt;/p&gt;

&lt;p&gt;If you build anything that reads text, try it on a language that doesn't use spaces. It is a cheap way to find assumptions you didn't know you had.&lt;/p&gt;




&lt;p&gt;About me: full-stack developer in South Korea, 13 years, open to remote contracts. I work fully in writing (async), so everything stays documented.&lt;br&gt;
CV: &lt;a href="https://cv.namubase.com" rel="noopener noreferrer"&gt;https://cv.namubase.com&lt;/a&gt;&lt;br&gt;
SearchLink: &lt;a href="https://searchlink.namubase.com" rel="noopener noreferrer"&gt;https://searchlink.namubase.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>testing</category>
      <category>webdev</category>
      <category>i18n</category>
    </item>
    <item>
      <title>I ship backends alone, in writing: 12 years, 6 products live right now</title>
      <dc:creator>Sangyoon Lee</dc:creator>
      <pubDate>Fri, 25 Sep 2026 00:38:29 +0000</pubDate>
      <link>https://dev.to/globalmatchhub_b7ad9cba/i-ship-backends-alone-in-writing-12-years-6-products-live-right-now-92p</link>
      <guid>https://dev.to/globalmatchhub_b7ad9cba/i-ship-backends-alone-in-writing-12-years-6-products-live-right-now-92p</guid>
      <description>&lt;p&gt;I am Lee Sangyoon, a full-stack engineer in South Korea. Twelve years, mostly in small teams where one person takes a feature from the spec to production and then answers for it afterwards. I am open to remote contract work and remote roles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One condition, stated first so nobody wastes time: I work in writing only.&lt;/strong&gt; Email, chat, issue trackers, pull requests. No calls, no video interviews. If that rules me out, it rules me out early, and that is fair to both sides. What I offer in exchange is a written trail of every decision, an answer within 24 hours, and a plan you can read before you pay for anything.&lt;/p&gt;

&lt;p&gt;And the written trail is the part I would keep even if calls were easy. Every request you make and every answer I give stays on the record. A change of scope and what it costs is written down the moment it is agreed, so it is not a memory contest three weeks later. You can search the whole history of the project, and hand it to whoever takes over from you. In my experience most disputes in contract work start with "but we said" about something nobody wrote down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work I was paid for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Children's news app (Flutter, iOS and Android), 2024 to 2026.&lt;/strong&gt; Shipped to both stores. Then rebuilt the backend from AWS Amplify to Spring Boot WebFlux while the app was live, as the only engineer. Because the API boundary was GraphQL, the app shipped no forced update during the migration. Recurring subscription billing through a payment gateway, Google and Apple sign-in, refresh-token auth. After launch: realtime ratings over GraphQL subscriptions, streak rewards, article translation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unmanned study cafe system, 2023.&lt;/strong&gt; The whole thing: web seat booking and passes, an Electron kiosk in the store, an admin for both, card payment with recurring billing, and a door controller wired to pass state so access reflects payment in realtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Healthcare platform, 2020 to 2023.&lt;/strong&gt; Rebuilt a hospital, patient and operator platform on Laravel 8 API plus React. Three role boundaries and SSO, API documented in Swagger so the app team could work in parallel, AWS setup and deploys run by me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Realtime chat app, 2019 to 2020.&lt;/strong&gt; Flutter and Firebase, FCM push, SMS-verified signup, in-app purchase with server-side receipt validation. Scaled from one server to four when traffic outgrew the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before that, 2014 to 2019.&lt;/strong&gt; A gold trading platform and its app, realtime chat and browser games, an eCRF solution for academic conferences, public sector sites, and a lot of commerce builds. PHP, jQuery and the habit of running my own servers came from here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side projects, all live, all built and operated solo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SearchLink&lt;/strong&gt; (&lt;a href="https://searchlink.namubase.com" rel="noopener noreferrer"&gt;https://searchlink.namubase.com&lt;/a&gt;). Connects Google Search Console to Claude and ChatGPT over MCP, mails a weekly report with three things to fix, and alerts when clicks drop. Cloudflare Workers, D1, OAuth, billing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public auction price statistics site.&lt;/strong&gt; A daily cron pulls a government open API and generates about 350 statistics pages. Workers plus D1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atlassian Marketplace apps.&lt;/strong&gt; A Jira markdown editor and dashboard gadgets, through Atlassian's security review and publicly installable. Forge and React.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketplace extensions&lt;/strong&gt; published on WordPress.org, JetBrains, VS Code, Figma and Wix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source.&lt;/strong&gt; An MCP server for Search Console on npm, listed in several MCP directories.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;TypeScript and Node, Python, PHP and Laravel, Java and Spring Boot (WebFlux), React, Next.js, Vue, Flutter, React Native, Electron. PostgreSQL, MySQL, Redis, SQLite, D1. AWS, Cloudflare Workers, Docker, Nginx, Linux. GraphQL and REST. Payment gateways, in-app purchase receipt validation, social login, FCM.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am looking for
&lt;/h2&gt;

&lt;p&gt;Remote contract work or a remote role, backend or full-stack, with written communication. Data pipelines, API integrations, LLM-backed tooling and "this thing must keep running unattended" work are what I enjoy most and what I am fastest at.&lt;/p&gt;

&lt;p&gt;Reach me at &lt;a href="mailto:globalmatchhub@gmail.com"&gt;globalmatchhub@gmail.com&lt;/a&gt;. Tell me what is broken or what you want built, and you will get a written plan back, not a meeting invite.&lt;/p&gt;

</description>
      <category>career</category>
      <category>remote</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why your Search Console API impressions don't match the UI</title>
      <dc:creator>Sangyoon Lee</dc:creator>
      <pubDate>Mon, 21 Sep 2026 04:38:18 +0000</pubDate>
      <link>https://dev.to/globalmatchhub_b7ad9cba/why-your-search-console-api-impressions-dont-match-the-ui-2cef</link>
      <guid>https://dev.to/globalmatchhub_b7ad9cba/why-your-search-console-api-impressions-dont-match-the-ui-2cef</guid>
      <description>&lt;p&gt;I was building a small tool that pulls Google Search Console data into Claude, and the first thing that broke was trust. My impressions total was higher than the number in the Search Console UI. Not by a rounding error. Noticeably higher.&lt;/p&gt;

&lt;p&gt;Here is what was going on, in case you hit the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake: summing page rows
&lt;/h2&gt;

&lt;p&gt;The obvious way to get totals is to query by &lt;code&gt;page&lt;/code&gt;, then add up clicks and impressions across all rows.&lt;/p&gt;

&lt;p&gt;Clicks come out close. Impressions do not.&lt;/p&gt;

&lt;p&gt;The reason: Search Console counts impressions differently depending on how you group the data. Grouped by property, one results page counts once. Grouped by page, every one of your URLs on that results page gets its own impression. If two of your pages show up for the same search, that is one impression for the site but two when you add up the page rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: take totals from the date dimension
&lt;/h2&gt;

&lt;p&gt;If you want numbers that match the UI, query with &lt;code&gt;dimensions: ["date"]&lt;/code&gt; and sum those rows instead. Each day is counted once at the property level, so impressions line up with what Search Console shows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"startDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"endDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use page or query dimensions for breakdowns ("which pages lost clicks"), and the date dimension for the headline totals. Don't mix the two in the same number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two smaller gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data lag.&lt;/strong&gt; The most recent two or three days are usually incomplete. If you compare "last 7 days" to "the 7 before", end both ranges a few days back, or the latest week always looks worse than it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average position is not additive.&lt;/strong&gt; You can't take a plain average of the position column across rows. Weight it by impressions, or take it from the same date-level query.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I ended up building
&lt;/h2&gt;

&lt;p&gt;I wrapped this into a small MCP server so I can ask Claude or Cursor things like "which pages lost clicks this month" without exporting a CSV first. It is read-only, runs locally, and takes totals from the date dimension.&lt;/p&gt;

&lt;p&gt;Disclosure: I built it. It's free and MIT: &lt;a href="https://go.namubase.com/slite?s=devto-0921" rel="noopener noreferrer"&gt;https://go.namubase.com/slite?s=devto-0921&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've found other places where the API and the UI disagree, I'd like to hear them.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
