<?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: Ruslan Hrekov</title>
    <description>The latest articles on DEV Community by Ruslan Hrekov (@hrekov).</description>
    <link>https://dev.to/hrekov</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%2F4153403%2Fd4606199-5154-429f-85e5-48070b3df40a.jpg</url>
      <title>DEV Community: Ruslan Hrekov</title>
      <link>https://dev.to/hrekov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hrekov"/>
    <language>en</language>
    <item>
      <title>No company, no API</title>
      <dc:creator>Ruslan Hrekov</dc:creator>
      <pubDate>Wed, 07 Oct 2026 05:52:10 +0000</pubDate>
      <link>https://dev.to/hrekov/no-company-no-api-59cd</link>
      <guid>https://dev.to/hrekov/no-company-no-api-59cd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fttte6xi286pnpnzue2l5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fttte6xi286pnpnzue2l5.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;The engagement agent on my SMM Factory pilot does two things while the owner is cutting hair: it leaves conversational comments on in-niche Instagram posts, and it replies to incoming DMs. Haiku writes them, the orchestrator rate-limits them, Playwright drives the browser session that posts them. No approval loop — this is the one path in the system that closes the human loop entirely.&lt;/p&gt;

&lt;p&gt;That whole stack is wrong by design. The right stack is Instagram Graph API through Meta Business Suite, with &lt;code&gt;instagram_manage_comments&lt;/code&gt; and &lt;code&gt;instagram_manage_messages&lt;/code&gt; write-scopes gated behind App Review. The reason I'm shipping on Playwright instead is one gate: Meta Business Verification requires a registered legal entity, and I was running this as a solo pilot without one.&lt;/p&gt;

&lt;p&gt;This post is about what that gate actually costs when you can't clear it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What App Review wants
&lt;/h2&gt;

&lt;p&gt;The public documentation makes App Review sound like a code review with a privacy policy attached. It isn't. For any Advanced Access scope — which includes anything that writes to another user's account — you have to pass Business Verification first. Business Verification wants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A registered legal entity with a verifiable address.&lt;/li&gt;
&lt;li&gt;A DUNS number, or a local-equivalent registration document (incorporation papers, VAT registration, trade license).&lt;/li&gt;
&lt;li&gt;Supporting documents on file — a utility bill or bank statement with the business name, matched against government registries Meta cross-references.&lt;/li&gt;
&lt;li&gt;A privacy policy published on an active production domain, not a Vercel preview URL.&lt;/li&gt;
&lt;li&gt;An explainer video of the exact user flow, demonstrating the scope's use case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an incorporated company this is a two-hour admin task. For a solo developer running a two-month pilot it's the whole deliverable, inverted. The gate didn't exist to filter me out specifically; it exists because this is Meta's single most-abused surface, and the business-entity check is a cheap way to make scaled spam uneconomic. By-design is the right word.&lt;/p&gt;

&lt;p&gt;Also — there is no sandbox mode for write-scopes. Standard Access gives you read-only testing against your own account. The moment you want to write to a client's inbox or feed, you need Advanced Access, and that path begins at Business Verification. You cannot ship a demo, show it to someone, and then fix the paperwork. The shipping step &lt;strong&gt;is&lt;/strong&gt; the paperwork.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I shipped instead
&lt;/h2&gt;

&lt;p&gt;A Playwright session with a long-lived IG cookie, driven by two agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;publisher&lt;/strong&gt; uses the cookie to post content on Mondays after a Slack approval.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;engagement agent&lt;/strong&gt; uses the same cookie to comment on target feeds daily and reply to DMs inline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvubimmd5hucpyy6je5jq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvubimmd5hucpyy6je5jq.png" alt="@eternalasting.event's Instagram post — a Barber Shop tent at a lakeside sunset event. Comments drawer open. The noble.booking account commented 16 weeks ago (hearted by author): 'That Hot Mess Bakery cake looks incredible with the barbershop theme, and the picnic setup is such a creative way to celebrate the anniversary with your crew.' The account owner replied '@noble.booking Thank you !' and hearted the exchange." width="800" height="1731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Comment landed · heart + thank-you from author&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngn1mblzf0exh7ic9nxk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngn1mblzf0exh7ic9nxk.png" alt="@citybarberib's Instagram post — four barbers on a hilltop at dusk. Comments drawer open. The noble.booking account commented 17 weeks ago (hearted by author): 'How do you keep your tools so clean working mobile like that?' The author replied '@noble.booking you mean the clippers or everything?' — the question earned a conversational back-and-forth, not a one-line acknowledgment." width="800" height="1731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Started a back-and-forth with the owner&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two auto-comments the engagement agent left on in-niche accounts (Prestige Barbershop event · City Barber). Both earned author replies and hearts. This is what the cookie-session fallback actually produces — Haiku-written, Playwright-posted, per-account rate-limited enough to pass as a human reader of the feed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Haiku generates the text. The orchestrator rate-limits by account and respects an interval floor between actions on the same thread. Everything is logged to Supabase so if an action pattern starts tripping IG's spam heuristics, the ledger can tell us exactly when the pattern changed.&lt;/p&gt;

&lt;p&gt;This works. It also has three real costs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cookie rotates.&lt;/strong&gt; When IG issues a new session cookie I have to re-authenticate manually. In the content pipeline logs you'll see this as &lt;code&gt;login_required&lt;/code&gt; on the trends endpoint — same mechanism, different scope. For the publisher I rotate on a weekly cadence; for engagement the ambient failure rate is higher because the surface is more active.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's outside Terms of Service.&lt;/strong&gt; IG's TOS doesn't make a distinction between a Playwright session with your own cookie and a scraper mimicking your account. If the pattern triggers rate-limit or spam heuristics, the account is paused. The right move at that point is to apologize to the client and accept it, not to look clever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It won't scale past a handful of clients.&lt;/strong&gt; Session cookies are per-account. If I'm running engagement for ten barbershops, I need ten cookies, ten weekly re-auth cycles, ten rate-limit windows to coordinate. The architecture is linear-cost in client count; the Graph API path would be constant-cost in the orchestrator and linear only in client API quotas.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the metrics lie about
&lt;/h2&gt;

&lt;p&gt;The results table on the pilot's case study has one line that reads cleaner than it should:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;0 IG violations · 2-month pilot&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a true number. It's also entirely a function of running under rate limits with one client. If I'd scaled the exact same architecture to five clients, the number would've gone up. If I'd shipped under the Graph API with proper scopes, the metric wouldn't exist — compliance is the baseline, not an achievement.&lt;/p&gt;

&lt;p&gt;Any time you see a safety metric reported on a cookie-driven system, read it as a statement about pacing, not architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you ship this way?
&lt;/h2&gt;

&lt;p&gt;Depends on exactly one question: are you running a pilot or building the product?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pilot.&lt;/strong&gt; One client, two-month window, honest conversation with the owner about where the automation touches their account. Playwright + cookie is defensible. You're testing whether the system is worth the paperwork before you buy the paperwork.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product.&lt;/strong&gt; Any plural number of clients, any plan to open a seat on a demo page. Register the entity, pass Business Verification, go through App Review. The paperwork isn't the cost of the API — it's the cost of writing to someone else's account at scale, and it's load-bearing on the whole product's story.&lt;/p&gt;

&lt;p&gt;The failure mode is doing a pilot's architecture past the pilot. The cookie that worked for one barbershop in a two-month window is the exact wrong substrate for anything after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Register the entity on day one, not day sixty. The entity doesn't gate the architecture decisions — I'd still have shipped the orchestrator, the Haiku stratification, the Slack approval, the knowledge-markdown system. What it gates is which API the production version runs against, and that question needs answering before the pipeline is load-bearing on a workaround.&lt;/p&gt;

&lt;p&gt;The pilot proved the system can run at cost. The thing it didn't prove — because the paperwork wasn't done — is whether the system could run at scale legally. Those are two different questions and I conflated them for most of the pilot.&lt;/p&gt;

&lt;p&gt;The clean API wasn't a lie. It was a filter, pointed exactly at builders like me, working exactly as designed.&lt;/p&gt;

</description>
      <category>meta</category>
      <category>instagram</category>
      <category>platformgates</category>
      <category>automation</category>
    </item>
    <item>
      <title>Shipping the cross-poster</title>
      <dc:creator>Ruslan Hrekov</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:37:52 +0000</pubDate>
      <link>https://dev.to/hrekov/shipping-the-cross-poster-34hn</link>
      <guid>https://dev.to/hrekov/shipping-the-cross-poster-34hn</guid>
      <description>&lt;p&gt;The previous post promised cross-posts to LinkedIn and dev.to "once Sprint 14 wires the automation." Three days later, Sprint 14 shipped and you're reading proof — this post landed on both platforms through a cron job the site runs on itself.&lt;/p&gt;

&lt;p&gt;Here's what shipping that pipeline looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The itch
&lt;/h2&gt;

&lt;p&gt;Cross-posting manually dies at the third post. Open LinkedIn, strip the markdown, re-check character count, paste. Open dev.to, remember the canonical URL field, paste the body, slugify the tags, confirm. Each post is fifteen minutes of friction between "I wrote something" and "the people who follow me on those platforms can read it." After two tries you stop writing instead of fixing it.&lt;/p&gt;

&lt;p&gt;The fix had to clear a lower bar than you'd think: it had to work, land in the correct places, and not accidentally publish the same post twice. The rest is polish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint
&lt;/h2&gt;

&lt;p&gt;One decision gated everything: no dedicated service, no Zapier, no VPS. The site already deploys on Vercel and the code already lives on GitHub, so the pipeline had to live inside the repo. GitHub Actions free tier cron, scripts in the same language as the site, state in a committed file. If the cron ever misbehaves, I want &lt;code&gt;git log&lt;/code&gt; to tell me when and why.&lt;/p&gt;

&lt;p&gt;That ruled out LinkedIn's partner-reviewed Community Management API (multi-week approval gate for a solo-developer app) and ruled in the self-serve &lt;code&gt;w_member_social&lt;/code&gt; scope targeting the legacy &lt;code&gt;/v2/ugcPosts&lt;/code&gt; endpoint. Different surface, same posting outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;Three hundred lines of orchestrator (&lt;code&gt;scripts/publish-due.mjs&lt;/code&gt;) read &lt;code&gt;content/blog/schedule.yml&lt;/code&gt;, cross-reference &lt;code&gt;.distribution-ledger.jsonl&lt;/code&gt;, and when a scheduled post is due and not already published, hand it to the matching publisher — &lt;code&gt;lib/distribution/devto.mjs&lt;/code&gt; or &lt;code&gt;lib/distribution/linkedin.mjs&lt;/code&gt;. On success, three things happen in a fixed order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Append to the ledger (&lt;code&gt;{slug, channel, publishedUrl, remoteId, ts}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Mutate the post's frontmatter (&lt;code&gt;status: posted&lt;/code&gt;, &lt;code&gt;publishedUrl&lt;/code&gt;, &lt;code&gt;postedAt&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Commit and push with &lt;code&gt;[skip ci]&lt;/code&gt; so Vercel still rebuilds but the cron origin is filterable in &lt;code&gt;git log&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ordering is non-negotiable. If step 2 crashes, step 1 already recorded the publish — the next cron tick sees the ledger entry and skips the slug instead of double-posting. Reversing to frontmatter-first means a crash leaves the post un-marked-as-published; the next tick re-publishes.&lt;/p&gt;

&lt;p&gt;The ledger is checked into git. Had to catch that one in review — my first draft had &lt;code&gt;.distribution-ledger.jsonl&lt;/code&gt; in &lt;code&gt;.gitignore&lt;/code&gt; because the filename looks like local state. GitHub Actions runners are ephemeral; an ignored ledger would see empty state on every tick and re-publish every past-scheduled post. The file isn't local state; it's the durable idempotency guard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three traps I almost shipped
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;/rest/posts&lt;/code&gt; vs &lt;code&gt;/v2/ugcPosts&lt;/code&gt;.&lt;/strong&gt; The LinkedIn docs lead you to the newer &lt;code&gt;/rest/posts&lt;/code&gt; under Community Management API. Looks modern, has nice typed schemas. Returns 403 with a self-serve token because that surface is partner-review-gated. The legacy UGC endpoint with &lt;code&gt;X-Restli-Protocol-Version: 2.0.0&lt;/code&gt; (and no &lt;code&gt;LinkedIn-Version&lt;/code&gt; header) is what self-serve apps actually get. A full afternoon vanished before I noticed the two docs sets live in parallel universes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refresh tokens don't exist on self-serve.&lt;/strong&gt; The access token is sixty days. The weekly refresh worker I wrote assumed a refresh token would arrive — none did. Not a bug; an empirical limit of the &lt;code&gt;w_member_social&lt;/code&gt; scope. Fix was a graceful no-op with a cliff-countdown warning, and a calendar reminder two weeks before expiry to re-run the OAuth handshake manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Company Pages aren't optional, but they're one-time.&lt;/strong&gt; My app wouldn't bind to a personal profile; LinkedIn's post-2019 verification chain wants a Page as the ownership anchor. Created a minimal Page in fifteen minutes, bound the app, then never used the Page again. All posts go to the personal profile via &lt;code&gt;urn:li:person:XKuTKDehCv&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runs now
&lt;/h2&gt;

&lt;p&gt;Hourly on &lt;code&gt;15 * * * *&lt;/code&gt;. Twenty-one seconds end-to-end on a cold GitHub Actions runner. On ticks with nothing due, the output is one line — &lt;code&gt;[publish-due] nothing due at &amp;lt;iso-timestamp&amp;gt;&lt;/code&gt; — and the job exits clean. On ticks with a due post, you see the publish lines, the devlog entry, and a cron commit landing in &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first live run shipped the previous post, &lt;a href="https://dev.to/blog/launching-the-journal"&gt;Launching the journal&lt;/a&gt;, to both platforms. That post went live manually because I needed to validate the renderers against real APIs before trusting the cron. This post is the first one going through the full automated path — authored, scheduled, pushed, published by the machine, with a commit back on &lt;code&gt;main&lt;/code&gt; to prove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this post itself proves
&lt;/h2&gt;

&lt;p&gt;You're reading a Build Log about a cross-posting pipeline. If you found it on dev.to or LinkedIn, that pipeline is what put it there — the schedule-driven path with no manual step between my &lt;code&gt;git push&lt;/code&gt; and your scroll.&lt;/p&gt;

&lt;p&gt;The whole thing is in the open: three hundred lines of orchestrator at &lt;a href="https://github.com/ruslan4427/portfolio/blob/main/scripts/publish-due.mjs" rel="noopener noreferrer"&gt;scripts/publish-due.mjs&lt;/a&gt;, four invariants locked into &lt;a href="https://github.com/ruslan4427/portfolio/blob/main/STABLE_LOGIC.md" rel="noopener noreferrer"&gt;STABLE_LOGIC.md&lt;/a&gt;, the full ship retro at &lt;a href="https://github.com/ruslan4427/portfolio/blob/main/DEVLOG.md" rel="noopener noreferrer"&gt;DEVLOG.md&lt;/a&gt;. Clone the repo, read the cron, run the dry-run — the pipeline isn't a demo of a clever idea, it's the thing that made this very post appear in two more places than I had to type.&lt;/p&gt;

</description>
      <category>meta</category>
      <category>automation</category>
      <category>distribution</category>
    </item>
    <item>
      <title>Launching the journal</title>
      <dc:creator>Ruslan Hrekov</dc:creator>
      <pubDate>Thu, 01 Oct 2026 02:27:51 +0000</pubDate>
      <link>https://dev.to/hrekov/launching-the-journal-3fh5</link>
      <guid>https://dev.to/hrekov/launching-the-journal-3fh5</guid>
      <description>&lt;p&gt;The site had five case studies and no place to put anything shorter. Everything I wanted to write about — a debugging trace, a pattern that keeps paying off, a skeptical read on a tool the timeline is excited about — needed the same weight as a full case study or it got dropped. That's the wrong shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/blog&lt;/code&gt; supports four formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Case Study&lt;/strong&gt; — long-form, receipts attached, commit hashes referenced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Log&lt;/strong&gt; — chronological, dated, mid-work. This post is one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field Note&lt;/strong&gt; — skeptical take on something popular. Short, arguable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern&lt;/strong&gt; — a rule I keep applying, why, and when it breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each post lands with reading time, tags, and (where relevant) a &lt;strong&gt;Receipts&lt;/strong&gt; aside — links to the actual commits, prompts, or artifacts behind the claims. If I can't cite it, I don't write it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MDX with frontmatter validated at build time (invalid frontmatter throws).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/rss.xml&lt;/code&gt; is generated from the same source with CDATA descriptions.&lt;/li&gt;
&lt;li&gt;Google Search Console meta tag ships via env var, so the feed shows up.&lt;/li&gt;
&lt;li&gt;Analytics: GA4 with IP anonymisation, off by default for EU visitors until you click accept. &lt;code&gt;proxy.ts&lt;/code&gt; (Next.js 16's edge middleware convention) reads Vercel's &lt;code&gt;x-vercel-ip-country&lt;/code&gt; header and sets a &lt;code&gt;geo-eu&lt;/code&gt; cookie so the client knows which path to render.&lt;/li&gt;
&lt;li&gt;No third-party tracking beyond that. No newsletter form yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;First real posts land over the next two weeks — one Field Note on prompt patterns that stopped working when I moved from Sonnet to Opus, one Build Log on Sprint 11 itself, and a Pattern write-up of &lt;code&gt;STABLE_LOGIC.md&lt;/code&gt; (the file that keeps AI collaboration from unwinding your own fixes).&lt;/p&gt;

&lt;p&gt;Subscribe via &lt;a href="///rss.xml"&gt;RSS&lt;/a&gt;, or wait for the LinkedIn + dev.to cross-posts — Sprint 14 wires the schedule-driven automation that lands this very post on both platforms.&lt;/p&gt;

</description>
      <category>meta</category>
      <category>portfolio</category>
    </item>
  </channel>
</rss>
