<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[yanmiayn blog]]></title><description><![CDATA[yanmiayn blog]]></description><link>https://yanmiayn-blog.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>yanmiayn blog</title><link>https://yanmiayn-blog.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 18 Sep 2026 21:43:03 GMT</lastBuildDate><atom:link href="https://yanmiayn-blog.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Resend got me Cloudflare 1010-banned. The 30-minute escape to Listmonk + AWS SES.]]></title><description><![CDATA[I sent 3 emails through Resend and got Cloudflare 1010-banned.
Not throttled. Not rate-limited. Banned. Error 1010, "the owner of this website has banned your access based on your browser's signature." From a transactional email API I had been paying...]]></description><link>https://yanmiayn-blog.hashnode.dev/resend-got-me-cloudflare-1010-banned-the-30-minute-escape-to-listmonk-aws-ses</link><guid isPermaLink="true">https://yanmiayn-blog.hashnode.dev/resend-got-me-cloudflare-1010-banned-the-30-minute-escape-to-listmonk-aws-ses</guid><category><![CDATA[AWS]]></category><category><![CDATA[Devops]]></category><category><![CDATA[email]]></category><category><![CDATA[#selfhosted]]></category><dc:creator><![CDATA[Yunchae Lee]]></dc:creator><pubDate>Tue, 05 May 2026 07:48:01 GMT</pubDate><content:encoded><![CDATA[<p>I sent 3 emails through Resend and got Cloudflare 1010-banned.</p>
<p>Not throttled. Not rate-limited. Banned. Error 1010, "the owner of this website has banned your access based on your browser's signature." From a transactional email API I had been paying for. From the same machine I had been using all week.</p>
<p>This is the story of the next 30 minutes, what I rebuilt with Listmonk + AWS SES, and the exact commands so you do not lose half a day if it happens to you.</p>
<h2 id="heading-what-actually-happened">what actually happened</h2>
<p>I run a small revenue stack on yanmiayn.com. Three emails that morning: one welcome, one receipt, one reply notification.</p>
<p>Around 09:14 KST the Resend dashboard stopped loading. Not a 500. The Cloudflare interstitial:</p>
<pre><code><span class="hljs-built_in">Error</span> <span class="hljs-number">1010</span>
Access denied
The owner <span class="hljs-keyword">of</span> <span class="hljs-built_in">this</span> website has banned your access
based on your browser<span class="hljs-string">'s signature.</span>
</code></pre><p><code>api.resend.com</code> returned the same. So did the docs. So did the status page.</p>
<pre><code class="lang-bash">curl -I https://api.resend.com/emails
<span class="hljs-comment"># HTTP/2 403</span>
<span class="hljs-comment"># server: cloudflare</span>
<span class="hljs-comment"># cf-mitigated: challenge</span>
</code></pre>
<p>LTE worked. So Cloudflare had fingerprinted my IP+TLS and decided I was a bot. Paying customer, no scrapers, no abuse. Locked out anyway.</p>
<h2 id="heading-the-debugging-spiral">the debugging spiral</h2>
<ol>
<li>Support ticket. Email-only. 24-48 hour reply window. My signup flow is already broken.</li>
<li>Resend status page green. Of course green. The ban is on me, not them.</li>
<li>Rotated home IP. Same 1010.</li>
<li>Residential proxy got past 1010, hit a different captcha. Gave up.</li>
</ol>
<p>The vendor and the gatekeeper are the same company. There is no appeal at startup speed.</p>
<p>I gave myself 30 minutes.</p>
<h2 id="heading-why-listmonk-aws-ses">why Listmonk + AWS SES</h2>
<ul>
<li>I own the sending reputation.</li>
<li>Runs on the box I already have.</li>
<li>Both transactional and bulk in one stack.</li>
<li>Deliverability not worse than Resend.</li>
<li>Bill stays low.</li>
</ul>
<p>Listmonk is an open source Go binary with an HTTP transactional API and a campaigns UI. AWS SES is the boring SMTP relay underneath at ~$0.10 per 1,000 emails.</p>
<h2 id="heading-the-30-minute-migration">the 30-minute migration</h2>
<h3 id="heading-minute-0-5-ses-domain-identity">minute 0-5: SES domain identity</h3>
<pre><code class="lang-bash">aws configure
aws ses verify-domain-identity --domain yanmiayn.com --region us-east-1
aws ses verify-domain-dkim --domain yanmiayn.com --region us-east-1
</code></pre>
<p>DNS:</p>
<pre><code>token1._domainkey.yanmiayn.com  CNAME  token1.dkim.amazonses.com
token2._domainkey.yanmiayn.com  CNAME  token2.dkim.amazonses.com
token3._domainkey.yanmiayn.com  CNAME  token3.dkim.amazonses.com

yanmiayn.com           TXT  <span class="hljs-string">"v=spf1 include:amazonses.com ~all"</span>
_dmarc.yanmiayn.com    TXT  <span class="hljs-string">"v=DMARC1; p=quarantine; rua=mailto:dmarc@yanmiayn.com; fo=1"</span>
</code></pre><p>Start with <code>p=quarantine</code>. Move to <code>p=reject</code> after a clean week.</p>
<h3 id="heading-minute-5-8-ses-smtp-credentials">minute 5-8: SES SMTP credentials</h3>
<pre><code class="lang-bash">aws iam create-user --user-name ses-smtp-listmonk

cat &gt; ses-send-policy.json &lt;&lt;<span class="hljs-string">'EOF'</span>
{
  <span class="hljs-string">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
  <span class="hljs-string">"Statement"</span>: [
    {
      <span class="hljs-string">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
      <span class="hljs-string">"Action"</span>: [<span class="hljs-string">"ses:SendRawEmail"</span>, <span class="hljs-string">"ses:SendEmail"</span>],
      <span class="hljs-string">"Resource"</span>: <span class="hljs-string">"*"</span>
    }
  ]
}
EOF

aws iam put-user-policy \
  --user-name ses-smtp-listmonk \
  --policy-name SesSendOnly \
  --policy-document file://ses-send-policy.json
</code></pre>
<p>Use the SES console "Create SMTP credentials" button to derive the SMTP username/password from the IAM access key.</p>
<h3 id="heading-minute-8-18-listmonk-on-docker">minute 8-18: Listmonk on Docker</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">app:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">listmonk/listmonk:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">listmonk_app</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"127.0.0.1:9000:9000"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">LISTMONK_app__address:</span> <span class="hljs-string">"0.0.0.0:9000"</span>
      <span class="hljs-attr">LISTMONK_db__host:</span> <span class="hljs-string">db</span>
      <span class="hljs-attr">LISTMONK_db__port:</span> <span class="hljs-number">5432</span>
      <span class="hljs-attr">LISTMONK_db__user:</span> <span class="hljs-string">listmonk</span>
      <span class="hljs-attr">LISTMONK_db__password:</span> <span class="hljs-string">${DB_PASSWORD}</span>
      <span class="hljs-attr">LISTMONK_db__database:</span> <span class="hljs-string">listmonk</span>
      <span class="hljs-attr">LISTMONK_db__ssl_mode:</span> <span class="hljs-string">disable</span>
      <span class="hljs-attr">LISTMONK_ADMIN_USER:</span> <span class="hljs-string">admin</span>
      <span class="hljs-attr">LISTMONK_ADMIN_PASSWORD:</span> <span class="hljs-string">${ADMIN_PASSWORD}</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">db</span>

  <span class="hljs-attr">db:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:16-alpine</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">listmonk_db</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">listmonk</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">${DB_PASSWORD}</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">listmonk</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./pgdata:/var/lib/postgresql/data</span>
</code></pre>
<pre><code class="lang-bash">cat &gt; .env &lt;&lt;EOF
DB_PASSWORD=$(openssl rand -hex 24)
ADMIN_PASSWORD=$(openssl rand -hex 16)
EOF
chmod 600 .env

docker compose up -d db
sleep 5
docker compose run --rm app ./listmonk --install --yes
docker compose up -d app
</code></pre>
<p>Reverse-proxy with Caddy:</p>
<pre><code>listmonk.yanmiayn.com {
    reverse_proxy <span class="hljs-number">127.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span>:<span class="hljs-number">9000</span>
}
</code></pre><h3 id="heading-minute-18-24-ses-into-listmonk">minute 18-24: SES into Listmonk</h3>
<p>Settings -&gt; SMTP -&gt; Add new:</p>
<pre><code>Host:     email-smtp.us-east<span class="hljs-number">-1.</span>amazonaws.com
<span class="hljs-attr">Port</span>:     <span class="hljs-number">587</span>
<span class="hljs-attr">Auth</span>:     login
<span class="hljs-attr">Username</span>: &lt;SES SMTP username&gt;
Password: &lt;SES SMTP password&gt;
TLS:      STARTTLS
</code></pre><p>Send a test. Check headers for <code>dkim=pass</code> and <code>spf=pass</code>.</p>
<h3 id="heading-minute-24-30-swap-the-transactional-api">minute 24-30: swap the transactional API</h3>
<pre><code class="lang-bash">curl -X POST https://listmonk.yanmiayn.com/api/tx \
  -u <span class="hljs-string">"admin:<span class="hljs-variable">$ADMIN_PASSWORD</span>"</span> \
  -H <span class="hljs-string">"Content-Type: application/json"</span> \
  -d <span class="hljs-string">'{
    "subscriber_email": "u@example.com",
    "template_id": 3,
    "data": {"name": "Joon"},
    "content_type": "html"
  }'</span>
</code></pre>
<p>Templates use Go syntax: <code>{{ .Subscriber.Email }}</code>, <code>{{ .Tx.Data.name }}</code>.</p>
<h2 id="heading-ses-sandbox-escape">SES sandbox escape</h2>
<p>New SES accounts: 200/day to verified addresses only. To go production: real website, specific description ("transactional only: receipts, password resets, signup confirmations"), honest volume, working unsubscribe. Approved in 14 hours for me.</p>
<p>Until then:</p>
<pre><code class="lang-bash">aws ses verify-email-identity --email-address you@yanmiayn.com --region us-east-1
</code></pre>
<h2 id="heading-what-i-gave-up">what I gave up</h2>
<ul>
<li>Bounce/complaint handling. With SES you wire SNS -&gt; Listmonk webhook yourself.</li>
<li>Polished dashboard. Listmonk's UI is functional.</li>
<li>Edge POPs. SES has regions. For most senders this does not matter.</li>
</ul>
<h2 id="heading-what-i-gained">what I gained</h2>
<ul>
<li>No vendor can lock me out of my own domain.</li>
<li>Bill from $20/mo to ~$1/mo at my volume.</li>
<li>Same stack handles 50k campaign sends and signup receipts.</li>
</ul>
<h2 id="heading-the-takeaway">the takeaway</h2>
<p>If you build on a transactional email API, write down a 30-minute migration plan today, before you need it. Park the Listmonk subdomain, the SES domain identity, and DNS records now. Keep the compose file in your infra repo.</p>
<p>Resend is fine. SendGrid is fine. Postmark is fine. They are all also one WAF rule away from being unavailable to you specifically. Owning the sender is small one-time work and real optionality.</p>
<p>If this saved you 4 hours, follow <a target="_blank" href="https://x.com/yanmiayn">@yanmiayn</a> for more boring infra escape hatches. Full runbook with SNS bounce wiring and a Node client wrapper at <a target="_blank" href="https://yanmiayn.com/cold-outreach-stack">yanmiayn.com/cold-outreach-stack</a> ($29, one-time).</p>
]]></content:encoded></item><item><title><![CDATA[A practical Claude workflow: Mike: open-source legal AI]]></title><description><![CDATA[Building with Claude: Mike: open-source legal AI
Recently the topic of "Mike: open-source legal AI" has been gaining attention among indie developers. Here is a practical workflow for tackling it with Claude.
1. Frame the problem clearly
Claude works...]]></description><link>https://yanmiayn-blog.hashnode.dev/a-practical-claude-workflow-mike-open-source-legal-ai</link><guid isPermaLink="true">https://yanmiayn-blog.hashnode.dev/a-practical-claude-workflow-mike-open-source-legal-ai</guid><dc:creator><![CDATA[Yunchae Lee]]></dc:creator><pubDate>Thu, 30 Apr 2026 21:20:13 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-building-with-claude-mike-open-source-legal-ai">Building with Claude: Mike: open-source legal AI</h1>
<p>Recently the topic of "Mike: open-source legal AI" has been gaining attention among indie developers. Here is a practical workflow for tackling it with Claude.</p>
<h2 id="heading-1-frame-the-problem-clearly">1. Frame the problem clearly</h2>
<p>Claude works best when you give it a tight scope, sample inputs, and the success criterion. Skip the wall of text — describe the smallest reproducible task.</p>
<h2 id="heading-2-use-a-prompt-template">2. Use a prompt template</h2>
<p>Reusable prompts are the difference between a one-off chat and a production system. A good template covers: role, context, constraints, examples, and an output contract.</p>
<h2 id="heading-3-iterate-with-diffs-not-rewrites">3. Iterate with diffs, not rewrites</h2>
<p>Ask Claude for a unified diff. It cuts token cost and makes review easier.</p>
<h2 id="heading-4-cache-the-system-prompt">4. Cache the system prompt</h2>
<p>For any SaaS feature using Claude, prompt caching turns a 4000-token system prompt into a near-free repeat call. Worth a 10-line refactor.</p>
<h2 id="heading-5-wrap-it-in-a-tiny-eval">5. Wrap it in a tiny eval</h2>
<p>Five hand-graded examples beat zero. Run them on every prompt change.</p>
<hr />
<p>If you want a head start, I put together 100 production-ready Claude prompts for SaaS builders (auth flows, billing logic, CRUD scaffolds, error handlers, etc.). It is a one-time $7.50 PDF: https://payhip.com/b/bXtWi</p>
]]></content:encoded></item><item><title><![CDATA[Should I use TypeScript or stay with JavaScript for a side project? — I asked 11 frontier LLMs]]></title><description><![CDATA[I run Consensus Engine — a tool that asks the same question to 11 frontier LLMs in parallel and synthesizes their answers into a single decision. Here's what they said about: Should I use TypeScript or stay with JavaScript for a side project?

Why th...]]></description><link>https://yanmiayn-blog.hashnode.dev/should-i-use-typescript-or-stay-with-javascript-for-a-side-project-i-asked-11-frontier-llms</link><guid isPermaLink="true">https://yanmiayn-blog.hashnode.dev/should-i-use-typescript-or-stay-with-javascript-for-a-side-project-i-asked-11-frontier-llms</guid><category><![CDATA[AI]]></category><category><![CDATA[devjournal]]></category><category><![CDATA[llm]]></category><category><![CDATA[Python]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Yunchae Lee]]></dc:creator><pubDate>Mon, 27 Apr 2026 14:03:53 GMT</pubDate><content:encoded><![CDATA[<p>I run <strong>Consensus Engine</strong> — a tool that asks the same question to 11 frontier LLMs in parallel and synthesizes their answers into a single decision. Here's what they said about: <em>Should I use TypeScript or stay with JavaScript for a side project?</em></p>
<blockquote>
<p>Why this matters: any single LLM can hallucinate or have blind spots. When 11 models agree, signal-to-noise jumps. When they disagree, the disagreement itself is information.</p>
</blockquote>
<h2 id="heading-the-synthesized-consensus">The synthesized consensus</h2>
<h1 id="heading-typescript-vs-javascript-recommendation">TypeScript vs JavaScript Recommendation</h1>
<h2 id="heading-consensus-recommendation">Consensus Recommendation</h2>
<p><strong>Start with JavaScript for your side project, but consider migrating to TypeScript as the project grows.</strong></p>
<p>All three models agree that <strong>project size and complexity</strong> should be the primary deciding factor. For small, experimental, or throwaway projects, JavaScript's simplicity and faster iteration speed make it ideal for side projects where you're often exploring ideas quickly.</p>
<h2 id="heading-key-considerations">Key Considerations</h2>
<p><strong>When to stick with JavaScript (2/3 models strongly agree):</strong></p>
<ul>
<li>Small scope or experimental projects</li>
<li>Quick prototypes you'll build and potentially discard</li>
<li>Projects where you're still exploring the problem space</li>
</ul>
<p><strong>When to use TypeScript (2/3 models strongly agree):</strong></p>
<ul>
<li>Projects likely to grow beyond a few files</li>
<li>Code you plan to maintain long-term</li>
<li>Complex applications that will evolve over time</li>
</ul>
<h2 id="heading-decision-framework">Decision Framework</h2>
<p>Use this decision tree:</p>
<ol>
<li><strong>Small/quick side project?</strong> → Use JavaScript</li>
<li><strong>Planning to maintain/expand it long-term?</strong> → Start with TypeScript</li>
<li><strong>Uncertain about complexity?</strong> → Start with JavaScript and consider migrating key components to TypeScript later</li>
</ol>
<h2 id="heading-actionable-strategy">Actionable Strategy</h2>
<ul>
<li><strong>JavaScript-first approach:</strong> Begin quickly, then migrate core components to TypeScript as complexity grows</li>
<li><strong>TypeScript-first approach:</strong> If you're committed to long-term maintenance or the project scope is inherently complex</li>
</ul>
<h2 id="heading-model-agreementdisagreement">Model Agreement/Disagreement</h2>
<p><strong>Agreed:</strong> All models
<strong>Divergence:</strong> The models differ on default recommendation - Nemotron-3 and Mistral favor TypeScript earlier, while Llama-4 suggests starting with whichever you know better</p>
<h2 id="heading-key-insight">Key Insight</h2>
<p>The strongest signal is that <strong>project scope and maintenance intentions should drive your choice</strong>. All models agree that TypeScript provides long-term benefits for maintainability and bug prevention, but the overhead may not be worth it for simple side projects you want to explore quickly.</p>
<p><strong>Start with JavaScript for rapid prototyping, but be prepared to migrate to TypeScript as your project grows in scope.</strong></p>
<h2 id="heading-what-each-model-said">What each model said</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Model</td><td>Excerpt</td></tr>
</thead>
<tbody>
<tr>
<td><code>mistral-large-3-675b-instruct-2512</code></td><td>Use TypeScript if your side project is medium to large, involves multiple developers, or will be maintained long-term. It catches errors early, improves code ma...</td></tr>
<tr>
<td><code>nemotron-3-super-120b-a12b</code></td><td>If your side project will grow beyond a few files or you plan to add features over time, start with TypeScript — its static types catch bugs early and improve I...</td></tr>
<tr>
<td><code>llama-4-maverick-17b-128e-instruct</code></td><td>For a side project, start with JavaScript if you're already familiar with it, as it allows for quicker setup and development. However, if you're looking to impr...</td></tr>
</tbody>
</table>
</div><h2 id="heading-try-it-yourself">Try it yourself</h2>
<p>The full interactive arena page (with all 11 raw responses, confidence score, and timing data) lives here:</p>
<p><strong>Canonical link:</strong> <a target="_blank" href="https://api.yanmiayn.com/arena/f2d29b00fffa">https://api.yanmiayn.com/arena/f2d29b00fffa</a></p>
<p>It's free, no signup, 10 questions/day per IP. Models rotate based on availability — typical run uses Llama 4, Mistral 675B, Qwen 3.5 397B, Qwen3-Coder 480B, Llama 3.1 405B, Nemotron Ultra 253B, GLM-5.1, DeepSeek V4 Pro, Kimi K2.5, MiniMax M2.7, and Phi-4.</p>
<h2 id="heading-why-publish-on-hashnode">Why publish on Hashnode?</h2>
<p>I'm building <strong>Consensus Engine</strong> in public. If you want to follow along — including the open-source python SDK at <a target="_blank" href="https://github.com/MICONNM/consensus-engine-py">github.com/MICONNM/consensus-engine-py</a> — subscribe to this blog. New post every 2 days, each one a real decision dissected by 11 AIs.</p>
<p><em>This article was generated automatically from the arena run linked above. The code that generated it is open-source: see <code>tools/hashnode_publisher.py</code> in the OpenClaw workspace.</em></p>
]]></content:encoded></item></channel></rss>