<?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: Tolga Cakular</title>
    <description>The latest articles on DEV Community by Tolga Cakular (@member_c98c2bbc).</description>
    <link>https://dev.to/member_c98c2bbc</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%2F4044083%2F9b63e5ed-db8e-4204-98ad-3e078f3f9e4b.png</url>
      <title>DEV Community: Tolga Cakular</title>
      <link>https://dev.to/member_c98c2bbc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/member_c98c2bbc"/>
    <language>en</language>
    <item>
      <title>Four things that bit us shipping a remote MCP server with OAuth</title>
      <dc:creator>Tolga Cakular</dc:creator>
      <pubDate>Sat, 15 Aug 2026 17:31:23 +0000</pubDate>
      <link>https://dev.to/member_c98c2bbc/four-things-that-bit-us-shipping-a-remote-mcp-server-with-oauth-1j91</link>
      <guid>https://dev.to/member_c98c2bbc/four-things-that-bit-us-shipping-a-remote-mcp-server-with-oauth-1j91</guid>
      <description>&lt;p&gt;We recently shipped a remote MCP server so people can verify email addresses from inside Claude, ChatGPT and Cursor. (Disclosure: I work on ClearBounce, the service behind it.) The protocol itself is pleasant to implement. What cost us time was everything &lt;em&gt;around&lt;/em&gt; it — the parts that are not in the spec, or are in the spec but that clients and directories disagree about.&lt;/p&gt;

&lt;p&gt;Here are four of them, with what we actually observed.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Echo the client's protocol version. Do not force your own.
&lt;/h2&gt;

&lt;p&gt;Our &lt;code&gt;initialize&lt;/code&gt; handler proudly returned the newest protocol version we supported. Claude Code refused to connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server's protocol version is not supported: 2026-07-28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client sends the version it wants in &lt;code&gt;params.protocolVersion&lt;/code&gt;. If you answer with something it has never heard of, it bails — even when your version is &lt;em&gt;newer&lt;/em&gt;. The fix is to accept any sane date-shaped version the client asks for and echo it back:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUPPORTED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-11-25&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-06-18&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-03-26&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;negotiateProtocolVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;requested&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/^20&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;SUPPORTED&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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;p&gt;Think of it as content negotiation, not as advertising your capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Return 401, not 403 — and accept that one directory will call you unhealthy for it
&lt;/h2&gt;

&lt;p&gt;For OAuth-protected servers, the discovery trigger is an unauthenticated request answered with &lt;code&gt;401&lt;/code&gt; plus a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header pointing at your protected-resource metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt; &lt;span class="ne"&gt;Unauthorized&lt;/span&gt;
&lt;span class="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource/mcp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what makes claude.ai and ChatGPT offer to connect an account instead of just failing. Return &lt;code&gt;403&lt;/code&gt; and the client has nothing to discover.&lt;/p&gt;

&lt;p&gt;We learned this the hard way in the other direction. An earlier version exposed an anonymous &lt;code&gt;tools/list&lt;/code&gt; as a friendly "here is what I can do" response. Because nothing ever returned 401, no client ever started the OAuth flow. Removing the anonymous showcase fixed it.&lt;/p&gt;

&lt;p&gt;The irony: after doing the correct thing, one directory's health checker connects without credentials, receives our correct 401, and marks the server &lt;strong&gt;Unhealthy&lt;/strong&gt; on its public listing. Meanwhile another directory's scanner documents the opposite requirement — its docs explicitly say servers should return 401 rather than 403 so OAuth can be discovered — and passed us cleanly.&lt;/p&gt;

&lt;p&gt;Same behaviour, two verdicts. Do the spec-correct thing, and be ready to explain the red badge.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Instrument the endpoint, or you are flying blind
&lt;/h2&gt;

&lt;p&gt;We added a small table that records one row per JSON-RPC request: method, tool name, client name, auth type, ok/error, duration. Fire-and-forget, so a failed insert can never break a tool call.&lt;/p&gt;

&lt;p&gt;The first day of data was not what we expected. Roughly 200 unauthenticated &lt;code&gt;initialize&lt;/code&gt; calls, from clients we had never heard of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcpbeat 0.1                  every ~15 minutes
smithery-probe
agent-tools.cloud 0.1
zdi-well-wirer 1.0
agentic-resource-search 0.4
trimtab-verifier 0.1
glama-mcp-inspector 1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We did not submit to most of these. There is an active crawler ecosystem discovering MCP endpoints on its own, and it is far busier than the human traffic. Someone also called &lt;code&gt;this/method/does/not/exist&lt;/code&gt;, presumably checking whether our error handling was sane.&lt;/p&gt;

&lt;p&gt;One practical detail: MCP is stateless, so only &lt;code&gt;initialize&lt;/code&gt; carries &lt;code&gt;clientInfo&lt;/code&gt;. Every other request would be anonymous. We fall back to the first token of the &lt;code&gt;User-Agent&lt;/code&gt; and keep it raw rather than mapping it to friendly product names — a guess would quietly produce wrong statistics. That is how we know a real call came from &lt;code&gt;claude-code/2.1.231&lt;/code&gt; rather than "probably Claude".&lt;/p&gt;

&lt;p&gt;If you are adding OAuth, also put the client id into the access token as a claim. Then every later call tells you which client it came from without any session state.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cursor's one-click install link is broken below 3.15.12
&lt;/h2&gt;

&lt;p&gt;We added the usual install deeplink to our docs page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cursor://anysphere.cursor-deeplink/mcp/install?name=NAME&amp;amp;config=BASE64_CONFIG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where the base64 payload for a remote server is simply:&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/mcp"&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;On an older Cursor build, clicking it brings the app to the front, opens the settings pane, and shows no install card at all. Nothing is added; nothing errors. It looks like your link is malformed.&lt;/p&gt;

&lt;p&gt;It is not. There is a bug report on Cursor's own forum describing exactly this, and a team member confirming it was fixed in 3.15.12+. If you ship one of these buttons, add a line telling people on older builds to add the server URL manually — otherwise the silent failure reads as your bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small stuff that also helped
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;outputSchema&lt;/code&gt; and the &lt;code&gt;readOnlyHint&lt;/code&gt; / &lt;code&gt;destructiveHint&lt;/code&gt; / &lt;code&gt;idempotentHint&lt;/code&gt; / &lt;code&gt;openWorldHint&lt;/code&gt; annotations to every tool. Directory scanners read them, and reviewers notice when they are missing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resources/list&lt;/code&gt; and &lt;code&gt;prompts/list&lt;/code&gt; will be probed even if you never declared those capabilities. Returning &lt;code&gt;-32601&lt;/code&gt; is spec-legal, but it shows up as a warning in scanner logs.&lt;/li&gt;
&lt;li&gt;If a tool costs the user money, say so in the tool description and tell the model to ask first. Ours returns a candidate list plus an explicit note about the per-check cost, and instructs the assistant to verify one at a time and stop at the first good result. Models follow that instruction well, and it is the difference between a useful tool and one that burns a stranger's credits.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;None of this is hard once you know it. It just is not written down in one place, which is why I wrote it down.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>oauth</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to verify email addresses in real time with a Node.js API</title>
      <dc:creator>Tolga Cakular</dc:creator>
      <pubDate>Thu, 23 Jul 2026 15:25:47 +0000</pubDate>
      <link>https://dev.to/member_c98c2bbc/how-to-verify-email-addresses-in-real-time-with-a-nodejs-api-8c7</link>
      <guid>https://dev.to/member_c98c2bbc/how-to-verify-email-addresses-in-real-time-with-a-nodejs-api-8c7</guid>
      <description>&lt;p&gt;Every signup form is a magnet for bad email addresses — typos like &lt;code&gt;jhon@gmial.com&lt;/code&gt;, throwaway inboxes, and addresses that simply don't exist. Let them into your list and you pay for it later: bounces, a wrecked sender reputation, and your real emails landing in spam.&lt;/p&gt;

&lt;p&gt;The fix is to &lt;strong&gt;verify an address before you trust it&lt;/strong&gt;. Here's how to do it in real time from Node.js.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "verifying" actually means
&lt;/h2&gt;

&lt;p&gt;A good check goes well beyond a regex. It looks at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Syntax&lt;/strong&gt; — is it a well-formed address?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MX records&lt;/strong&gt; — does the domain actually accept mail?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTP&lt;/strong&gt; — does the specific mailbox exist?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disposable&lt;/strong&gt; — is it a 10-minute throwaway?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-based&lt;/strong&gt; — &lt;code&gt;info@&lt;/code&gt;, &lt;code&gt;support@&lt;/code&gt; (low engagement)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch-all&lt;/strong&gt; — the domain accepts everything, so you can't be 100% sure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typos&lt;/strong&gt; — &lt;code&gt;gmial.com&lt;/code&gt; should probably be &lt;code&gt;gmail.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Calling the API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// verify.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLEARBOUNCE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.clearbounce.net/api/v1/verify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Verify failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;p&gt;A response looks like this:&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deliverable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isDeliverable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checks"&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;span class="nl"&gt;"syntax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mxRecords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"smtpValid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isDisposable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isRoleBased"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isCatchAll"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hasTypo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;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;&lt;code&gt;status&lt;/code&gt; is one of &lt;code&gt;deliverable&lt;/code&gt;, &lt;code&gt;undeliverable&lt;/code&gt;, &lt;code&gt;risky&lt;/code&gt;, or &lt;code&gt;unknown&lt;/code&gt;, and &lt;code&gt;score&lt;/code&gt; (0–100) tells you how confident the result is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gating a signup with it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undeliverable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;That inbox doesn't exist — please double-check it.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isDisposable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please use a permanent email address.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// "risky" (catch-all / role) → let them in, but flag it for a later re-check&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emailRisk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;risky&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;h2&gt;
  
  
  Two habits that keep your list clean
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify at the point of entry&lt;/strong&gt; (real time, like above) so junk never gets in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-clean the whole list periodically&lt;/strong&gt; (bulk) — even good addresses go stale over time.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Full disclosure: I build &lt;a href="https://clearbounce.net" rel="noopener noreferrer"&gt;ClearBounce&lt;/a&gt;, the verification API I used in the examples (it has a free tier if you want to try it). But the ideas matter more than the vendor — the same flow works with any real-time verification API.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
