<?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: Alex</title>
    <description>The latest articles on DEV Community by Alex (@fuxuxscheduler).</description>
    <link>https://dev.to/fuxuxscheduler</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%2F4175443%2F0a3718ce-a0c5-41dd-91ea-a3d4cca388f4.png</url>
      <title>DEV Community: Alex</title>
      <link>https://dev.to/fuxuxscheduler</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fuxuxscheduler"/>
    <language>en</language>
    <item>
      <title>We went from 33 to 87 on Cloudflare's agent-readiness scan. Here's what each check actually does.</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:33:12 +0000</pubDate>
      <link>https://dev.to/fuxuxscheduler/we-went-from-33-to-87-on-cloudflares-agent-readiness-scan-heres-what-each-check-actually-does-4a24</link>
      <guid>https://dev.to/fuxuxscheduler/we-went-from-33-to-87-on-cloudflares-agent-readiness-scan-heres-what-each-check-actually-does-4a24</guid>
      <description>&lt;p&gt;Cloudflare has a free scanner at &lt;a href="https://isitagentready.com" rel="noopener noreferrer"&gt;isitagentready.com&lt;/a&gt; that grades how easy your site is for AI agents to find, read and use. We build &lt;a href="https://www.fuxux.com" rel="noopener noreferrer"&gt;Fuxux&lt;/a&gt;, a social media scheduler with a public REST API and a hosted MCP server, so I assumed we'd do fine.&lt;/p&gt;

&lt;p&gt;We scored &lt;strong&gt;33/100, Level 1 ("Basic Web Presence")&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A day later we were at &lt;strong&gt;87/100, Level 5 ("Agent-Native")&lt;/strong&gt;. None of it was hard, but most of it wasn't obvious either, so here's every check that failed, what it means, and the exact thing we now serve. Two checks still fail on purpose, and I'll explain why at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Discoverability&lt;/td&gt;
&lt;td&gt;2/4&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content&lt;/td&gt;
&lt;td&gt;1/1&lt;/td&gt;
&lt;td&gt;1/1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot Access Control&lt;/td&gt;
&lt;td&gt;1/2&lt;/td&gt;
&lt;td&gt;2/2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API, Auth, MCP &amp;amp; Skill Discovery&lt;/td&gt;
&lt;td&gt;1/8&lt;/td&gt;
&lt;td&gt;6/8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What we already had: a valid &lt;code&gt;robots.txt&lt;/code&gt;, a sitemap, and Markdown versions of our pages for agents (that's the Content check). What we didn't have was any machine-readable way to say &lt;em&gt;"there's an API here, and an MCP server, and this is how you sign in"&lt;/em&gt;. We had all of those things. We just never told agents where they were.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Link headers on the homepage (RFC 8288)
&lt;/h2&gt;

&lt;p&gt;The cheapest fix. Agents that only fetch your homepage can't find anything that isn't linked from it, and HTML links are noisy. A &lt;code&gt;Link&lt;/code&gt; response header points straight at the useful resources:&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="err"&gt;Link: &amp;lt;/.well-known/api-catalog&amp;gt;; rel="api-catalog",
      &amp;lt;/openapi.json&amp;gt;; rel="service-desc"; type="application/vnd.oai.openapi+json",
      &amp;lt;/docs&amp;gt;; rel="service-doc"; type="text/html",
      &amp;lt;/.well-known/mcp/server-card.json&amp;gt;; rel="describedby"; type="application/json",
      &amp;lt;/llms.txt&amp;gt;; rel="describedby"; type="text/plain"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're on Next.js, so this is one entry in &lt;code&gt;headers()&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt;, applied to &lt;code&gt;/&lt;/code&gt; and our translated homepages.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Content Signals in robots.txt
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;robots.txt&lt;/code&gt; says &lt;em&gt;who&lt;/em&gt; may crawl. &lt;a href="https://contentsignals.org" rel="noopener noreferrer"&gt;Content Signals&lt;/a&gt; say &lt;em&gt;what they may do with it&lt;/em&gt;: show it in search, use it as input to an AI answer, or train on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-Agent: *
Content-Signal: search=yes, ai-input=yes, ai-train=yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a business decision, not a technical one. We want AI assistants to know Fuxux exists and recommend it, so everything is &lt;code&gt;yes&lt;/code&gt;. A news site would probably answer differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. An API catalog (RFC 9727)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/.well-known/api-catalog&lt;/code&gt; is a small JSON "linkset" listing your APIs and where their descriptions and docs live. Ours lists the REST API and the MCP server:&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;"linkset"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"anchor"&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://www.fuxux.com/api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"service-desc"&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;"href"&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://www.fuxux.com/openapi.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/vnd.oai.openapi+json"&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;"service-doc"&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;"href"&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://www.fuxux.com/docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text/html"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"anchor"&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://www.fuxux.com/api/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"service-desc"&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;"href"&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://www.fuxux.com/.well-known/mcp/server-card.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&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;"service-doc"&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;"href"&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://www.fuxux.com/mcp/docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text/html"&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;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;Serve it as &lt;code&gt;application/linkset+json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The MCP server card was missing one field
&lt;/h2&gt;

&lt;p&gt;We already published &lt;code&gt;/.well-known/mcp.json&lt;/code&gt;, and the scanner found it, then failed it: &lt;em&gt;"missing required fields (need serverInfo.name or name)"&lt;/em&gt;. We'd written it for one client months ago and never checked it against the draft server-card format.&lt;/p&gt;

&lt;p&gt;We added &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt; and a &lt;code&gt;serverInfo&lt;/code&gt; block, kept every old field so nothing that read it before breaks, and also serve it at &lt;code&gt;/.well-known/mcp/server-card.json&lt;/code&gt;:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.fuxux/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fuxux"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.3.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"serverInfo"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fuxux"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fuxux"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.3.0"&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;"transport"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"streamable-http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"endpoint"&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://www.fuxux.com/api/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;span class="nl"&gt;"authentication"&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;"required"&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;"schemes"&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;"oauth2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bearer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protected_resource_metadata"&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://www.fuxux.com/.well-known/oauth-protected-resource"&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;(Trimmed: the real one also has a description, icons and docs links.)&lt;/p&gt;

&lt;h2&gt;
  
  
  5. An Agent Skills index
&lt;/h2&gt;

&lt;p&gt;Agent Skills are Markdown instruction files (&lt;code&gt;SKILL.md&lt;/code&gt;) that teach an agent how to use your product. We already had two for the ChatGPT plugin and ClawHub. The discovery index just lists them, with a hash so a client can tell whether a file changed:&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;"$schema"&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://schemas.agentskills.io/discovery/0.2.0/schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"skills"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"social-media-posts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"skill-md"&lt;/span&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;"/.well-known/agent-skills/social-media-posts/SKILL.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"digest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:4f7590cc…"&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;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;One gotcha: compute the digest from the exact bytes you serve. We first hashed the file before our build changed its line endings, and the digest didn't match.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. An ARD manifest
&lt;/h2&gt;

&lt;p&gt;ARD (Agentic Resource Discovery) is one catalog at &lt;code&gt;/.well-known/ai-catalog.json&lt;/code&gt; that lists everything an agent can use on your domain: MCP servers, OpenAPI specs, agents. Each entry can carry &lt;code&gt;representativeQueries&lt;/code&gt;, example requests the resource is good for:&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="nl"&gt;"representativeQueries"&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="s2"&gt;"schedule a post to Instagram and LinkedIn for tomorrow at 9am"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"what posts do I have scheduled this week"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"why did my TikTok post fail"&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;h2&gt;
  
  
  7. WebMCP: two read-only tools in the browser
&lt;/h2&gt;

&lt;p&gt;WebMCP lets a page register tools that an AI agent running &lt;em&gt;in the user's browser&lt;/em&gt; can call. It's still a draft and few browsers support it, so we kept it tiny: two read-only tools on marketing pages, one returning an overview, one returning pricing, both from Markdown we already serve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&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="nx"&gt;modelContext&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&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;ctx&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registerTool&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// most browsers today&lt;/span&gt;
&lt;span class="k"&gt;for &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;tool&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// an older draft of the API: leave the page alone&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;No writes, no auth, nothing that can change an account. If the API changes shape, the &lt;code&gt;try&lt;/code&gt; keeps the page working.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. DNS for AI discovery, and DNSSEC
&lt;/h2&gt;

&lt;p&gt;This one took the longest, and it isn't code. DNS-AID lets an agent find your services with a DNS lookup instead of a web request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_mcp._agents.fuxux.com.   HTTPS 1 www.fuxux.com. alpn="h2" port=443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding the record took a minute. The scanner still failed it, with a precise message: &lt;em&gt;"records found, but DNSSEC was not validated."&lt;/em&gt; A discovery record only counts if a resolver can prove it wasn't forged.&lt;/p&gt;

&lt;p&gt;Our DNS is on Cloudflare but the domain is registered at GoDaddy, so DNSSEC has two halves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Turn DNSSEC on in Cloudflare (DNS → Settings). It signs the zone and shows you a DS record.&lt;/li&gt;
&lt;li&gt;Add that DS record at the registrar (GoDaddy → DNS → DS Records): key tag, algorithm 13, digest type 2, digest.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;About 20 minutes later &lt;code&gt;cloudflare-dns.com&lt;/code&gt; returned &lt;code&gt;AD: true&lt;/code&gt; for the record, and Discoverability went to 4/4. Side benefit: the whole domain is now protected against forged DNS answers, which we should have done anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two checks we skip on purpose
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;OAuth Protected Resource Metadata at the root.&lt;/strong&gt; We do serve RFC 9728 metadata, but ours names the MCP endpoint as the resource:&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;"resource"&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://www.fuxux.com/api/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"authorization_servers"&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;"https://www.fuxux.com"&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;The scanner expects the document at the root to name the origin itself. That's a reasonable reading of the RFC. But when we submitted our connector to Claude's and ChatGPT's directories, both reviews needed &lt;code&gt;resource&lt;/code&gt; to equal the MCP URL exactly. One scanner point isn't worth risking two live directory listings, so this stays as it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;auth.md.&lt;/strong&gt; This lets an agent register an account on its own, without a person going through sign-up. For a product where each published X post costs us real money, and where our abuse limits (one trial per person, email and card checks) assume a human signs up, automatic account creation is a product decision, not a checkbox. We'll revisit it when assistants actually use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do first if you're starting from Level 1
&lt;/h2&gt;

&lt;p&gt;In order of effort versus score:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;Content-Signal&lt;/code&gt; in robots.txt (two minutes)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Link&lt;/code&gt; headers on the homepage (ten minutes)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/.well-known/api-catalog&lt;/code&gt; if you have an API&lt;/li&gt;
&lt;li&gt;Check any MCP server card you already publish against the current format&lt;/li&gt;
&lt;li&gt;DNSSEC, if your DNS provider makes it one click&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then decide, deliberately, which checks you &lt;em&gt;don't&lt;/em&gt; want. The score is a checklist, not a goal. A couple of the things it asks for change who can do what on your product, and those deserve a real decision.&lt;/p&gt;

&lt;p&gt;If you want to try the MCP server this was all about, it's at &lt;code&gt;https://www.fuxux.com/api/mcp&lt;/code&gt;, and the setup for Claude, ChatGPT and Cursor is at &lt;a href="https://www.fuxux.com/mcp/docs" rel="noopener noreferrer"&gt;fuxux.com/mcp/docs&lt;/a&gt;. Questions about any of the files above are welcome in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>Social Media Schedulers That Work in ChatGPT and Claude (2026)</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 22 Oct 2026 21:14:55 +0000</pubDate>
      <link>https://dev.to/fuxuxscheduler/social-media-schedulers-that-work-in-chatgpt-and-claude-2026-3he9</link>
      <guid>https://dev.to/fuxuxscheduler/social-media-schedulers-that-work-in-chatgpt-and-claude-2026-3he9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Independent guide from Fuxux, which is one of the tools compared. We are not affiliated with any other tool here. Every price, plan limit and directory listing was read from the vendor's own pages, or from the Claude and ChatGPT directories, on 22 October 2026. These change quickly, so check before you buy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A year ago, "AI social media tool" meant a caption generator. Now you can ask Claude or ChatGPT to write a week of posts, and the chat schedules them in your scheduler. That works through a connector built on MCP (Model Context Protocol), which gives the assistant a small set of tools: list your accounts, create a post, schedule it, check the queue.&lt;/p&gt;

&lt;p&gt;Plenty of schedulers now say they "work with AI". Fewer let you connect from Claude or ChatGPT with a simple sign-in. Fewer still do it on a free plan. This guide compares ten of them on the questions that decide whether it works for you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; five tools let you connect Claude or ChatGPT on a free plan: Buffer, Metricool, Pallyy, Typefully and Fuxux. Their free limits differ a lot, from 5 posts a month to 10 queued posts per channel. Postiz, Post Bridge, Publer, Vista Social and Hootsuite need a paid plan for it, or have no free plan at all. Pick by the networks you post to and how the price is counted, not by the AI feature, which is now similar across most of them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What "works with ChatGPT and Claude" actually means
&lt;/h2&gt;

&lt;p&gt;There are three levels, and vendor pages often blur them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listed in the assistant's directory.&lt;/strong&gt; In Claude you add it from the connectors directory; in ChatGPT, from the plugin directory. You sign in to the scheduler and approve access, with no key to copy. This is the easiest route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hosted MCP server you add by hand.&lt;/strong&gt; You paste a server URL as a custom connector. This still uses a sign-in on most tools, but in ChatGPT a custom connector needs a paid ChatGPT plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An API key.&lt;/strong&gt; This is for developer tools such as Claude Code or Cursor, or for automations. It is powerful, but often sits behind a higher plan or an add-on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The questions that matter: can you connect with a sign-in, is it on the plan you can afford, which networks will it post to, and can it publish or only draft?&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;AI connection on the free plan?&lt;/th&gt;
&lt;th&gt;Claude directory&lt;/th&gt;
&lt;th&gt;ChatGPT directory&lt;/th&gt;
&lt;th&gt;Cheapest paid plan&lt;/th&gt;
&lt;th&gt;Networks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fuxux&lt;/td&gt;
&lt;td&gt;Yes (3 accounts, 5 posts a month, no X)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$29/mo flat, 15 accounts&lt;/td&gt;
&lt;td&gt;14, incl. Telegram and Slack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buffer&lt;/td&gt;
&lt;td&gt;Yes (3 channels, 10 scheduled posts each)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;$6/mo per channel ($5 billed yearly)&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metricool&lt;/td&gt;
&lt;td&gt;Yes (1 brand, 20 posts a month, no LinkedIn or X)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;From $25/mo ($20 yearly), up to 5 brands&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pallyy&lt;/td&gt;
&lt;td&gt;Yes (1 social set, 15 posts a month, images only)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$25/mo per social set&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typefully&lt;/td&gt;
&lt;td&gt;Yes (1 social set, 10 posts a month)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not found (custom connector)&lt;/td&gt;
&lt;td&gt;$99/yr, billed yearly only&lt;/td&gt;
&lt;td&gt;6, text-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postiz&lt;/td&gt;
&lt;td&gt;No free plan (7-day trial)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$29/mo ($23 yearly), 5 channels&lt;/td&gt;
&lt;td&gt;30+, incl. Telegram and Slack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post Bridge&lt;/td&gt;
&lt;td&gt;No (MCP from the $39 plan)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$39/mo for MCP, 15 accounts&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publer&lt;/td&gt;
&lt;td&gt;No (Business plan and up)&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;Business about $8 per social account a month, billed yearly&lt;/td&gt;
&lt;td&gt;13, incl. Telegram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vista Social&lt;/td&gt;
&lt;td&gt;No (all paid plans)&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;$99/mo, 15 profiles&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hootsuite&lt;/td&gt;
&lt;td&gt;No free plan (14-day trial)&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;€99 per user/mo billed yearly; plan with publishing MCP not stated&lt;/td&gt;
&lt;td&gt;Not stated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Not found" means we searched the directory on 22 October 2026 and the tool did not come up. Several of these tools can still be added by hand as a custom connector.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fuxux
&lt;/h3&gt;

&lt;p&gt;Fuxux is our tool, so read this with that in mind. It is listed in both Claude's connector directory and ChatGPT's plugin directory: you sign in and approve, and there is no key to copy. The connection is on every plan, including Free.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What the assistant can do:&lt;/strong&gt; list your connected accounts, publish now, schedule, save a draft, edit or reschedule a scheduled post, list the queue and read a short dashboard of stats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networks:&lt;/strong&gt; 14. Instagram, TikTok, YouTube, X, Facebook, LinkedIn, Threads, Pinterest, Bluesky, Discord, Twitch, Kick, Telegram and Slack. Telegram and Slack are rare in this list; of the free options here, only Fuxux posts to them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; flat, not per channel. Free is 3 accounts and 5 published posts a month (each network a post goes to counts as one) and does not include X. Creator is $29 a month for 15 accounts and unlimited posts. An API key, for Cursor or scripts, is a $5 a month add-on on paid plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak spot:&lt;/strong&gt; the free plan's 5 posts a month is the smallest here. It is enough to try the AI workflow, not to run a busy account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Buffer
&lt;/h3&gt;

&lt;p&gt;Buffer's MCP page says access is "included on every plan, including Free", and it connects with a sign-in. Its tools cover sharing now, adding to the queue, custom scheduling, drafts, ideas and post metrics. The free plan gives 3 channels with 10 scheduled posts each, the most room of any free option here. Paid plans are counted per channel, which is cheap for a few accounts and adds up for many. It was in Claude's directory but did not come up in ChatGPT's when we searched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metricool
&lt;/h3&gt;

&lt;p&gt;Metricool is in both directories and connects with a sign-in on any plan, Free included. It is the strongest pick if your AI questions are about numbers: its connector covers analytics, ads accounts and best times to post as well as scheduling. The catch is the free plan: 1 brand and 20 posts a month, and its pricing page says it covers every network "except LinkedIn and Twitter".&lt;/p&gt;

&lt;h3&gt;
  
  
  Pallyy
&lt;/h3&gt;

&lt;p&gt;Pallyy says its connector works "on every plan, free included", and it is in both directories. The assistant can create draft or scheduled posts and import images. Pallyy's own docs say it "cannot read analytics", and the free plan is 1 social set with 15 image posts a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typefully
&lt;/h3&gt;

&lt;p&gt;Typefully is built for writers on X, LinkedIn, Threads, Bluesky, Mastodon and Substack. Its free plan includes "Agents, API, MCP" with 10 posts a month, and it has a verified badge in Claude's directory. In ChatGPT it is a custom connector rather than a directory listing. If you mostly write text posts and threads, it is a good fit; if you post video to TikTok or Instagram, it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The paid options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Postiz
&lt;/h3&gt;

&lt;p&gt;Postiz covers the most networks (30+, Telegram and Slack included) and has a hosted MCP server on every plan. It is in both directories. There is no free plan: you get a 7-day trial, then plans start at $29 a month for 5 channels ($23 billed yearly). It is also open source, if you would rather host it yourself and handle each network's app approval.&lt;/p&gt;

&lt;h3&gt;
  
  
  Post Bridge
&lt;/h3&gt;

&lt;p&gt;Post Bridge lists "MCP agent connect" on its $39 a month Marketer plan (15 accounts), and it is in both directories. Its connector has 16 tools, including media upload and analytics sync. It posts to 10 networks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Publer
&lt;/h3&gt;

&lt;p&gt;Publer's MCP is on the Business and Enterprise plans. Business is priced per social account (about $8 a month each, billed yearly, on the page we saw). It connects as a custom connector, and it was not in either directory when we checked. It does post to Telegram.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vista Social
&lt;/h3&gt;

&lt;p&gt;Vista Social's connector is "included with all paid plans" and advertises more than 100 tools across scheduling, analytics, inbox and reporting, the widest tool set here. Plans start at $99 a month for 15 profiles. It is aimed at teams and agencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hootsuite
&lt;/h3&gt;

&lt;p&gt;Hootsuite has separate MCP servers for publishing, inbox and listening. Its plans page lists only the listening connector, on Enterprise, so we could not confirm which plan includes publishing through Claude or ChatGPT. Standard starts at €99 per user a month, billed yearly. If you are already on Hootsuite, ask your account manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you...&lt;/th&gt;
&lt;th&gt;Look at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Want to try it free with a few accounts&lt;/td&gt;
&lt;td&gt;Buffer (most free posts) or Fuxux (14 networks, sign-in from both directories)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ask your AI mostly about analytics and ads&lt;/td&gt;
&lt;td&gt;Metricool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post to Telegram channels or Slack along with social&lt;/td&gt;
&lt;td&gt;Fuxux, or Postiz if you need even more networks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write text posts and threads for X, LinkedIn and Threads&lt;/td&gt;
&lt;td&gt;Typefully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manage many accounts and want one flat price&lt;/td&gt;
&lt;td&gt;Fuxux ($29 for 15 accounts) or Post Bridge ($39 for 15)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run an agency with inbox and reporting needs&lt;/td&gt;
&lt;td&gt;Vista Social or Hootsuite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two more checks before you commit. First, count your posts, not just your accounts: a free plan with 5 or 10 posts a month disappears fast when one post goes to four networks. Second, check that the assistant can &lt;em&gt;publish&lt;/em&gt;, not only draft, if you want it to run on its own. Some tools stop at drafts on purpose, which is safer but means you finish the job in their app.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to connect one in five minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an account with the scheduler and connect your social accounts there first. The assistant can only post where the scheduler can.&lt;/li&gt;
&lt;li&gt;In Claude, open Settings, then Connectors, and browse the directory. In ChatGPT, open Plugins and search. Add the tool, sign in and approve access.&lt;/li&gt;
&lt;li&gt;If the tool is not listed, add it as a custom connector with the server URL from its docs. In ChatGPT this needs a paid plan.&lt;/li&gt;
&lt;li&gt;Start with a safe request: "List my connected accounts", then "Draft a LinkedIn post about X and save it as a draft". Check the draft in the scheduler before you let the assistant publish.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Fuxux, the step-by-step guides are on &lt;a href="https://www.fuxux.com/claude?ref=devto" rel="noopener noreferrer"&gt;Fuxux for Claude&lt;/a&gt; and &lt;a href="https://www.fuxux.com/chatgpt?ref=devto" rel="noopener noreferrer"&gt;Fuxux for ChatGPT&lt;/a&gt;, and the technical details are in the &lt;a href="https://www.fuxux.com/mcp/docs?ref=devto" rel="noopener noreferrer"&gt;MCP docs&lt;/a&gt;. For background on how this works, see &lt;a href="https://www.fuxux.com/blog/how-to-schedule-social-media-posts-with-mcp?ref=devto" rel="noopener noreferrer"&gt;how to schedule social media posts with MCP&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can ChatGPT post to Instagram or TikTok by itself?
&lt;/h3&gt;

&lt;p&gt;Not by itself. ChatGPT needs a scheduler connected as a plugin or connector, and that scheduler holds the permission to post to your accounts. Instagram and TikTok also need the right account type: an Instagram professional account, and for TikTok, a scheduler approved for direct posting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need an API key?
&lt;/h3&gt;

&lt;p&gt;Not for Claude or ChatGPT with the tools in the directories: you sign in and approve. API keys are for developer tools such as Cursor or Claude Code with a header, and for scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it safe to let an AI assistant post for me?
&lt;/h3&gt;

&lt;p&gt;Start with drafts and review them. The assistant only gets the tools the scheduler exposes, and you can remove the connection at any time from the assistant's settings or the scheduler. Treat "publish now" like giving a colleague the keys: fine once you trust the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which tool has the most free posts?
&lt;/h3&gt;

&lt;p&gt;Among the free options with a sign-in connection, Buffer: 3 channels with 10 scheduled posts each. Metricool allows 20 a month but leaves out LinkedIn and X.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which ones post to Telegram or Slack?
&lt;/h3&gt;

&lt;p&gt;From this list, Fuxux and Postiz post to both, and Publer posts to Telegram. Fuxux is the only one of them with a free plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;The AI part has become table stakes: most good schedulers now give Claude and ChatGPT the same basic tools. Choose on the plain things: whether you can connect with a sign-in on the plan you will actually pay for, the networks you need, and how the price grows with your accounts. If you want to try the workflow today without a card, &lt;a href="https://www.fuxux.com/sign-up?ref=devto" rel="noopener noreferrer"&gt;Fuxux's free plan&lt;/a&gt; connects to both assistants in a couple of minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.fuxux.com/blog/social-media-schedulers-for-chatgpt-and-claude?ref=devto" rel="noopener noreferrer"&gt;Fuxux blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>chatgpt</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stripe told us a plan changed. It hadn't.</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 19 Oct 2026 09:44:55 +0000</pubDate>
      <link>https://dev.to/fuxuxscheduler/stripe-told-us-a-plan-changed-it-hadnt-5bkk</link>
      <guid>https://dev.to/fuxuxscheduler/stripe-told-us-a-plan-changed-it-hadnt-5bkk</guid>
      <description>&lt;p&gt;We run a small social media scheduler, and the events we care about send us a message in Telegram: a new sign-up, a new subscription, a plan change, a billing error. It's a cheap way to know what's happening without opening a dashboard.&lt;/p&gt;

&lt;p&gt;Last Friday afternoon one of those messages said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Plan changed: Creator, monthly&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody had changed anything. The subscription was on the same plan, at the same price, as the day before. What had actually happened was much more boring: a 7-day trial had ended and the first paid month had started.&lt;/p&gt;

&lt;p&gt;The alert was wrong, and the reason is a detail of Stripe webhooks that is easy to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the alert code did
&lt;/h2&gt;

&lt;p&gt;Plan changes come in as &lt;code&gt;customer.subscription.updated&lt;/code&gt;. That event fires for a lot of reasons (status changes, renewals, metadata edits), so we only wanted to alert when the plan itself changed. Stripe sends &lt;code&gt;previous_attributes&lt;/code&gt; with every update: the fields that changed, with their old values. So the check looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previousAttributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;previous_attributes&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Subscription&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;previous_attributes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// "items changed" = "the customer switched plans"&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;previousAttributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;notifyOpsSubscription&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plan_change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&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;The reasoning: the plan lives on the subscription item's price, so if &lt;code&gt;items&lt;/code&gt; is in &lt;code&gt;previous_attributes&lt;/code&gt;, the plan must have changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Stripe actually sent
&lt;/h2&gt;

&lt;p&gt;When the trial ended, the update looked roughly like this (trimmed):&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer.subscription.updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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;"object"&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;"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;"active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"items"&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;"data"&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;"price"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"price_creator_monthly"&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;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;"previous_attributes"&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;"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;"trialing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"items"&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;"data"&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;"price"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"price_creator_monthly"&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;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="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;items&lt;/code&gt; is there, but the price inside it is the same. In recent Stripe API versions the billing period (&lt;code&gt;current_period_start&lt;/code&gt; and &lt;code&gt;current_period_end&lt;/code&gt;) lives on the subscription item, not on the subscription. When a trial ends, the period moves from the trial week to the first paid month. The item changed, so &lt;code&gt;items&lt;/code&gt; shows up in &lt;code&gt;previous_attributes&lt;/code&gt;, even though nobody touched the plan.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;previous_attributes&lt;/code&gt; tells you &lt;strong&gt;which top-level field&lt;/strong&gt; changed. It doesn't tell you &lt;strong&gt;what&lt;/strong&gt; changed inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Compare the value you care about instead of checking that the field exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previousPriceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;previousAttributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;items&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Alert only when the price itself changed (an upgrade or downgrade).&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;previousPriceId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;previousPriceId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;priceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;notifyOpsSubscription&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plan_change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&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;Now a trial ending, a renewal or a period change is just an update. A real switch from one price to another still alerts. Billing itself was never affected: the plan we store comes from the current price on every update, so only the notification was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd check in your own webhook
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search for &lt;code&gt;previous_attributes?.something&lt;/code&gt; used as a boolean.&lt;/strong&gt; Each one is "this field changed somehow", which is rarely the question you're asking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff the exact value you care about&lt;/strong&gt;: the price ID for plan changes, &lt;code&gt;status&lt;/code&gt; for activations, &lt;code&gt;cancel_at_period_end&lt;/code&gt; for cancellations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the trial end, not just the checkout.&lt;/strong&gt; Most of us test "subscribe" and "cancel" by hand and never sit through a trial. Stripe's test clocks let you move a test subscription past its trial end in a few seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect nested objects to change for reasons that aren't yours.&lt;/strong&gt; Stripe moves fields between versions, and anything nested (items, discounts, invoice settings) can appear in &lt;code&gt;previous_attributes&lt;/code&gt; after an API upgrade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The alert that caught this was a false alarm, but it did its job: it made us look. If it had been an email to the customer saying "your plan changed", it would have been a support ticket instead.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We build &lt;a href="https://www.fuxux.com/?ref=devto" rel="noopener noreferrer"&gt;Fuxux&lt;/a&gt;, a social media scheduler for creators. This bug came out of our own billing alerts last week.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webhooks</category>
      <category>typescript</category>
      <category>saas</category>
    </item>
    <item>
      <title>string.length is wrong for X and Bluesky. My counter failed 8 of 34 tests against X's own library</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 12 Oct 2026 16:55:59 +0000</pubDate>
      <link>https://dev.to/fuxuxscheduler/stringlength-is-wrong-for-x-and-bluesky-my-counter-failed-8-of-34-tests-against-xs-own-library-kpk</link>
      <guid>https://dev.to/fuxuxscheduler/stringlength-is-wrong-for-x-and-bluesky-my-counter-failed-8-of-34-tests-against-xs-own-library-kpk</guid>
      <description>&lt;p&gt;I'm Alex. I build &lt;a href="https://www.fuxux.com" rel="noopener noreferrer"&gt;Fuxux&lt;/a&gt;, a social media scheduler: you write a post once and it goes out to X, Bluesky, Instagram, LinkedIn, Threads and a few more.&lt;/p&gt;

&lt;p&gt;A scheduler has to answer one boring question before anything is sent: does this text fit? I assumed that was &lt;code&gt;text.length &amp;lt;= limit&lt;/code&gt;. It isn't, and the two networks with the smallest limits are the two where it goes wrong.&lt;/p&gt;

&lt;p&gt;I also thought I had already solved it. Then I sat down to write this post, checked my own code against X's library to get the examples right, and it was wrong on 8 of 34 strings. So this is the corrected version, with the mistake left in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to count one string
&lt;/h2&gt;

&lt;p&gt;Take &lt;code&gt;ok 👍🏽&lt;/code&gt;. That's "ok", a space, and a thumbs up with a skin tone.&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok 👍🏽&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// 7  UTF-16 code units&lt;/span&gt;
&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 5  code points&lt;/span&gt;
&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Segmenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;granularity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grapheme&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="nf"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 4  what a person sees&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three honest answers. Which one a network uses decides whether your post is accepted.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;String&lt;/th&gt;
&lt;th&gt;&lt;code&gt;length&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;X counts&lt;/th&gt;
&lt;th&gt;Bluesky counts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ok 👍🏽&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;👨‍👩‍👧‍👦&lt;/code&gt; (family)&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;🇪🇸&lt;/code&gt; (flag)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;こんにちは&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;𝗯𝗼𝗹𝗱&lt;/code&gt; (bold "font")&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;read fuxux.com now&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a 140-character URL&lt;/td&gt;
&lt;td&gt;140&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;140&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The X column is from X's own library. The Bluesky column is the grapheme count.&lt;/p&gt;

&lt;h2&gt;
  
  
  X: weights, not characters
&lt;/h2&gt;

&lt;p&gt;X publishes how it counts in an open source library, &lt;a href="https://github.com/twitter/twitter-text" rel="noopener noreferrer"&gt;twitter-text&lt;/a&gt;. The limit is 280, but 280 of a weighted unit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latin letters, digits and common punctuation weigh 1. In code point terms: &lt;code&gt;U+0000&lt;/code&gt; to &lt;code&gt;U+10FF&lt;/code&gt;, &lt;code&gt;U+2000&lt;/code&gt; to &lt;code&gt;U+200D&lt;/code&gt;, &lt;code&gt;U+2010&lt;/code&gt; to &lt;code&gt;U+201F&lt;/code&gt; and &lt;code&gt;U+2032&lt;/code&gt; to &lt;code&gt;U+2037&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Everything else weighs 2. That's why five Japanese characters cost 10, and why those bold "fonts" people paste into bios cost double: they are mathematical letters far outside the Latin range.&lt;/li&gt;
&lt;li&gt;An emoji weighs 2, as a whole. A family of four is seven code points glued with joiners and it is still 2.&lt;/li&gt;
&lt;li&gt;Any link weighs 23, whether it is 9 characters or 140.&lt;/li&gt;
&lt;li&gt;The text is normalised to NFC first, so &lt;code&gt;e&lt;/code&gt; plus a combining accent counts as one &lt;code&gt;é&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The link rule bites in two directions. A long URL is cheaper than it looks. A short one is more expensive: &lt;code&gt;fuxux.com&lt;/code&gt; is 9 characters and costs 23.&lt;/p&gt;

&lt;p&gt;And it catches text you didn't mean as a link. &lt;code&gt;It works.So does this&lt;/code&gt; is 21 characters. X counts 36, because a missing space after the full stop makes &lt;code&gt;works.So&lt;/code&gt; look like a domain (&lt;code&gt;.so&lt;/code&gt; is a real ending).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I got it wrong
&lt;/h2&gt;

&lt;p&gt;My first version walked the string one code point at a time and added 1 or 2:&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;function&lt;/span&gt; &lt;span class="nf"&gt;weigh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;char&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;xWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codePointAt&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;total&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;That handles a plain 👍 (one code point, weight 2). It falls apart on anything built from several:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;String&lt;/th&gt;
&lt;th&gt;X&lt;/th&gt;
&lt;th&gt;My first version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ok 👍🏽&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;👨‍👩‍👧‍👦&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;🇪🇸&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1️⃣&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;❤️&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It always over-counted, so nobody could post something too long because of it. It just told people their post didn't fit when it did. On a 280 limit, a few emoji with skin tones is enough to matter.&lt;/p&gt;

&lt;p&gt;The fix is to split into graphemes first, and count a grapheme that is an emoji as 2 no matter what it is made of:&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;segmenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Segmenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;granularity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grapheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Drawn as an emoji by default, or a symbol followed by the emoji&lt;/span&gt;
&lt;span class="c1"&gt;// selector, or a flag letter, or a keycap like 1️⃣.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EMOJI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{Emoji_Presentation}|&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{Extended_Pictographic}️|&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{Regional_Indicator}|&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9#*&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;️&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;⃣/u&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;xWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cp&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mh"&gt;0x10ff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x2000&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mh"&gt;0x200d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x2010&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mh"&gt;0x201f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x2032&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mh"&gt;0x2037&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&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;weigh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;segment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;segmenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;EMOJI&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;segment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for &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;char&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;xWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codePointAt&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&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;countForX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urlPattern&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;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NFC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlPattern&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;weigh&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="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&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="nx"&gt;length&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;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;weigh&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="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last&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;My second attempt was also wrong, in a smaller way. I used &lt;code&gt;\p{Extended_Pictographic}&lt;/code&gt; on its own, and &lt;code&gt;© 2026&lt;/code&gt; came out as 7 instead of 6. A bare © is in that Unicode class, but X only treats it as an emoji when the emoji selector (&lt;code&gt;U+FE0F&lt;/code&gt;) follows it. Same for a text heart &lt;code&gt;❤&lt;/code&gt; versus &lt;code&gt;❤️&lt;/code&gt;. Hence the longer pattern above.&lt;/p&gt;

&lt;p&gt;It now matches twitter-text on 43 of my 44 test strings. The one it misses is a link ending I don't carry: X knows every domain ending there is, and I keep a short list of the common ones, so &lt;code&gt;a.community&lt;/code&gt; is a link to X and plain text to me. If you need that last case, use their list.&lt;/p&gt;

&lt;p&gt;Which is the real advice here: if you can ship the dependency, call &lt;code&gt;twitter.parseTweet(text).weightedLength&lt;/code&gt; and be done. I wanted something small that runs in the browser as you type, so I wrote my own and test it against theirs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bluesky: graphemes, and a second limit behind it
&lt;/h2&gt;

&lt;p&gt;Bluesky's limit is 300, and it counts graphemes. The post schema says so directly: &lt;code&gt;maxGraphemes: 300&lt;/code&gt;. So the family emoji is 1, the flag is 1, and &lt;code&gt;ok 👍🏽&lt;/code&gt; is 4. &lt;code&gt;Intl.Segmenter&lt;/code&gt; gives you that number in every current browser and in Node.&lt;/p&gt;

&lt;p&gt;The same schema also has &lt;code&gt;maxLength: 3000&lt;/code&gt;, and that one is in UTF-8 bytes. Normal text never gets near it. Three hundred family emoji would, at 25 bytes each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest
&lt;/h2&gt;

&lt;p&gt;For Instagram, LinkedIn, Threads, Facebook and the others I count &lt;code&gt;text.length&lt;/code&gt;. I have not checked each of them emoji by emoji the way I just did for X, so I won't claim more than that. Their limits are large enough (500 on Threads, 2,200 on Instagram, 3,000 on LinkedIn) that a few units either way rarely decides anything. On 280 and 300 it does.&lt;/p&gt;

&lt;p&gt;If you only need the numbers, I keep the limits for all 12 networks in &lt;a href="https://sites.google.com/view/social-media-character-limits" rel="noopener noreferrer"&gt;one table&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The counter is free and needs no account: &lt;a href="https://www.fuxux.com/tools/character-counter" rel="noopener noreferrer"&gt;fuxux.com/tools/character-counter&lt;/a&gt;. Paste a post and it shows the count for each network side by side, using the rules above. Paste &lt;code&gt;ok 👍🏽&lt;/code&gt; and you'll see X and Bluesky disagree.&lt;/p&gt;

&lt;p&gt;The limits in it are the same numbers Fuxux uses when it publishes. If you'd rather not think about any of this, that's what the &lt;a href="https://www.fuxux.com" rel="noopener noreferrer"&gt;scheduler&lt;/a&gt; is for, and there's a free plan (3 social accounts, 5 posts a month, no card).&lt;/p&gt;

&lt;p&gt;If you know a string where X's count surprised you, put it in the comments. I'll add it to the tests.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>unicode</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>What it took to get an MCP server into Claude's directory (and what broke along the way)</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:39:00 +0000</pubDate>
      <link>https://dev.to/fuxuxscheduler/what-it-took-to-get-an-mcp-server-into-claudes-directory-and-what-broke-along-the-way-10hc</link>
      <guid>https://dev.to/fuxuxscheduler/what-it-took-to-get-an-mcp-server-into-claudes-directory-and-what-broke-along-the-way-10hc</guid>
      <description>&lt;p&gt;I'm Alex. I build &lt;a href="https://www.fuxux.com" rel="noopener noreferrer"&gt;Fuxux&lt;/a&gt; on my own. It's a social media scheduler: you write a post once and it goes out to Instagram, TikTok, YouTube, LinkedIn, X, Threads, Bluesky, Pinterest and a few more, each with its own caption.&lt;/p&gt;

&lt;p&gt;I wanted one thing: to open Claude, type "schedule this for LinkedIn tomorrow at 9", and have it actually happen. Fuxux already had an MCP server, but it worked with an API key you paste into your client. Fine for Cursor. Useless for Claude's directory and for ChatGPT, because there is nowhere to paste a key. They want your server to handle sign-in.&lt;/p&gt;

&lt;p&gt;Most of the work turned out to be small things nobody tells you. So here they are, in the order I hit them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. My server was failing the wrong way
&lt;/h2&gt;

&lt;p&gt;When a request came in without credentials, I returned a 200 with an error message inside. Felt reasonable. It isn't. A client that supports sign-in only starts the flow when it gets a real &lt;code&gt;401&lt;/code&gt; plus a header telling it where to look:&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://www.fuxux.com/.well-known/oauth-protected-resource", scope="mcp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without that header the client just says the connection failed and gives you nothing else to go on.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Two small JSON files that have to be exactly right
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/.well-known/oauth-protected-resource&lt;/code&gt; says "this is the resource, and this server protects it". The &lt;code&gt;resource&lt;/code&gt; value must be the exact URL people connect to. Mine is &lt;code&gt;https://www.fuxux.com/api/mcp&lt;/code&gt;. A missing &lt;code&gt;www&lt;/code&gt; or an extra slash at the end is enough to break it, and nothing tells you why.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt; lists your authorize, token and registration endpoints, says you support PKCE with &lt;code&gt;S256&lt;/code&gt;, and that public clients are fine (&lt;code&gt;token_endpoint_auth_methods_supported: ["none"]&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Claude and ChatGPT don't register the way I expected
&lt;/h2&gt;

&lt;p&gt;I expected every client to register itself with dynamic client registration, because that's what the older examples show. Then I connected Claude and it never called it.&lt;/p&gt;

&lt;p&gt;Claude sends a client id that is a URL. You fetch that URL and it gives you a small document with the client's name and where to send the user back. ChatGPT does the same thing with its own document. So you need to support both ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client ID Metadata Documents&lt;/strong&gt;, for Claude and ChatGPT. Put &lt;code&gt;client_id_metadata_document_supported: true&lt;/code&gt; in your metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic registration&lt;/strong&gt;, for the tools that still use it. Don't skip it: when I submitted to another catalog recently, I saw a company pull their own submission because their server didn't have it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ChatGPT had one more surprise for me. Its document says &lt;code&gt;token_endpoint_auth_method: "private_key_jwt"&lt;/code&gt; and lists &lt;code&gt;["none", "private_key_jwt"]&lt;/code&gt; as supported. My code only accepted &lt;code&gt;none&lt;/code&gt; as the method, so ChatGPT users got my "this sign-in request is not valid" page. The fix was to accept any client that lists &lt;code&gt;none&lt;/code&gt; as supported, and treat it as a public client with PKCE.&lt;/p&gt;

&lt;p&gt;And their review scan checks your tools. Every tool has to say &lt;code&gt;readOnlyHint&lt;/code&gt;, &lt;code&gt;destructiveHint&lt;/code&gt; and &lt;code&gt;openWorldHint&lt;/code&gt; out loud. It flagged my "update post" tool because I had marked it as not destructive. Fair. Editing a post can overwrite a caption you can't get back.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Not every app comes back to an https address
&lt;/h2&gt;

&lt;p&gt;Claude Code and other terminal tools listen on &lt;code&gt;http://localhost&lt;/code&gt; and pick a new port each time. If you compare the return address as an exact string, a sign-in works once and fails the next time. For localhost you have to ignore the port (it's in RFC 8252).&lt;/p&gt;

&lt;p&gt;Some apps come back to their own scheme, like &lt;code&gt;something://callback&lt;/code&gt;. I don't accept those yet, only https and localhost. So those apps still use an API key with Fuxux for now. I wrote that on the setup page instead of pretending.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Turning sign-in on took one of my listings down
&lt;/h2&gt;

&lt;p&gt;This one I didn't see coming. A directory that checks my server every hour suddenly marked it as unhealthy. Nothing was broken. Their check calls the server with no credentials, which used to get a 200 and now got a 401. I had to go into their dashboard and authorise a test account. I only knew because they emailed me.&lt;/p&gt;

&lt;p&gt;If you're listed anywhere, check your listings the day you switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The consent screen has to be true
&lt;/h2&gt;

&lt;p&gt;When someone connects Claude to Fuxux, the screen tells them what Claude will be able to do. I made sure the code enforces it, not just the text:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a connected assistant gets 8 tools: your posts (list, read, create, edit, delete), your connected accounts, your dashboard, and "who am I"&lt;/li&gt;
&lt;li&gt;it can't touch your plan, billing, password or connected accounts&lt;/li&gt;
&lt;li&gt;you can remove it in Settings and it stops working right then&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also cut down what the tools send back. "Who am I" returns your email, name and plan. Nothing else leaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The part that had nothing to do with OAuth
&lt;/h2&gt;

&lt;p&gt;The listing went live and I had my best day ever: 12 sign-ups, 10 of them from Claude. That's more than I usually get in a week.&lt;/p&gt;

&lt;p&gt;Two days later I looked at what those people had done. One of the 12 had connected a social account. One.&lt;/p&gt;

&lt;p&gt;It took me a while to see why. You find Fuxux in Claude's directory, click connect, sign in on my consent screen, approve, and you're sent straight back to Claude. You now have a Fuxux account and you have never seen Fuxux. No onboarding. Nobody asked you to connect Instagram. So you ask Claude to schedule a post and there's nothing to post to.&lt;/p&gt;

&lt;p&gt;What I changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if a tool is called and you have no accounts connected, the answer now tells you what to do next instead of giving back an empty list&lt;/li&gt;
&lt;li&gt;one email, sent once, after you approve the connector with nothing connected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's still on my list is a "connect your first account" step right after you approve.&lt;/p&gt;

&lt;p&gt;So if you're building for one of these directories: test it as someone who has never been on your site. That's who shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do with it now
&lt;/h2&gt;

&lt;p&gt;Once it's connected, this is the kind of thing you can ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What do I have scheduled this week, and did anything fail?"&lt;/li&gt;
&lt;li&gt;"Write a post about our new pricing page, one version for LinkedIn and one for Bluesky, and save both as drafts"&lt;/li&gt;
&lt;li&gt;"Move tomorrow's Instagram post to Friday at 9"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works from Claude (it's in &lt;a href="https://claude.ai/directory/fuxux" rel="noopener noreferrer"&gt;Claude's directory&lt;/a&gt;), Claude Code, ChatGPT and Cursor, and there's a setup page for Codex too. Each one takes about a minute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.fuxux.com/claude" rel="noopener noreferrer"&gt;Fuxux for Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fuxux.com/chatgpt" rel="noopener noreferrer"&gt;Fuxux for ChatGPT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fuxux.com/agents" rel="noopener noreferrer"&gt;all assistants&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a free plan: 3 social accounts and 5 posts a month, no card. Enough to try the whole thing from your assistant and see if it fits how you work.&lt;/p&gt;

&lt;p&gt;If you're building your own MCP server and get stuck on any of the steps above, ask in the comments. I've probably hit it.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
