<?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: Azamat Safarov</title>
    <description>The latest articles on DEV Community by Azamat Safarov (@azamat_safarov).</description>
    <link>https://dev.to/azamat_safarov</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%2F3863993%2Ff1776dd4-1d71-443d-b276-97f2eebedd15.jpg</url>
      <title>DEV Community: Azamat Safarov</title>
      <link>https://dev.to/azamat_safarov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/azamat_safarov"/>
    <language>en</language>
    <item>
      <title>Chrono-Ledger Architecture: A Technical Deep-Dive into State Registries on Public Blockchains</title>
      <dc:creator>Azamat Safarov</dc:creator>
      <pubDate>Sat, 19 Sep 2026 22:25:02 +0000</pubDate>
      <link>https://dev.to/azamat_safarov/chrono-ledger-architecture-a-technical-deep-dive-into-state-registries-on-public-blockchains-2fk0</link>
      <guid>https://dev.to/azamat_safarov/chrono-ledger-architecture-a-technical-deep-dive-into-state-registries-on-public-blockchains-2fk0</guid>
      <description>&lt;p&gt;State registries — land cadastres, corporate databases, identity systems — are among the oldest information systems in existence. They also share a hidden architectural assumption that would be considered unacceptable in any other distributed system: that the custodian won't rewrite history.&lt;/p&gt;

&lt;p&gt;Together with Maxim Osovsky, I published a paper that formalizes an alternative: Chrono-Ledger Architecture. This post is a technical deep-dive — what follows is the architecture as the paper develops it, from archival foundations through vulnerability pathways to the formal model, invariants, threat model, and sovereignty argument.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=7460243" rel="noopener noreferrer"&gt;Full paper on SSRN.&lt;/a&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%2Fsvzxs15hjkqrk1p4tj2f.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%2Fsvzxs15hjkqrk1p4tj2f.png" alt="Chrono-Ledger: from ancient manuscript to blockchain registry" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Archival Resonances: Registries as Political Memory
&lt;/h2&gt;

&lt;p&gt;The paper opens with a thesis that recordkeeping belongs within the machinery of public authority, not at its administrative margin. This has a deep institutional pedigree: Aristotle placed a magistracy for written returns of private contracts among the indispensable offices of the polis. Roman &lt;em&gt;ius archivi&lt;/em&gt; joined preservation, evidentiary authority, and sovereign recognition. Hobbes located registry officials among the public ministers through whom the sovereign acts. Weber treated the files as a defining component of the modern bureau.&lt;/p&gt;

&lt;p&gt;Three traditions in archival theory structure the problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuity (Leibniz).&lt;/strong&gt; The archive as an instrument of government — a record binds the future only if it remains intact, retrievable, and usable as evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production (Foucault, Stoler).&lt;/strong&gt; The archive determines what can appear as a statement at all. Archival practice determines what counts as ownership, which documents prove it, and which claims remain invisible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authority (Derrida, Mbembe).&lt;/strong&gt; Archival control is archontic — those who guard it control access, classification, and interpretation. Mbembe: there is no state without its archives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In an ordinary registry, these three dimensions — continuity, production, and authority — are collapsed into one institutional point. A single custodian decides what may be recorded, how it is interpreted, and whether it remains available. This concentration creates the condition the paper calls &lt;strong&gt;documentary death&lt;/strong&gt;: loss or capture of a record resulting in loss or capture of legal status. Documentary death occurs when three criteria are met simultaneously: (i) the record cannot be restored through ordinary legal process, (ii) authorized parties cannot reconstruct the prior state, and (iii) no competent authority remains to adjudicate claims based on the record.&lt;/p&gt;

&lt;p&gt;From these three dimensions, the paper derives three architectural requirements: continuity requires immutable and independently verifiable history; production requires the current legal status to remain reproducible from that history; authority requires every registration, amendment, rectification, or cancellation to identify the competent institution, while separating that power from control over the canonical history.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Four Vulnerability Pathways of State Memory
&lt;/h2&gt;

&lt;p&gt;The paper organizes registry failures around four pathways, each analyzed as a pair of &lt;strong&gt;problem and architectural response&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Physical destruction.&lt;/strong&gt; If the record is materially concentrated, destroying the medium destroys the proof. Maya codices at Maní, 1562. Armenian property archives in 1915. 44 tons of apartheid records destroyed before the TRC. Post-Soviet privatization in the 1990s: when documentary infrastructure is uncertain, records become weapons for control over assets. Chrono-Ledger responds by requiring the current registry state to be reproducible from a preserved history of legally authorized transitions — replication prevents one custodian's destruction from destroying the evidentiary basis of the state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Custodial capture.&lt;/strong&gt; Institutional centralization creates an archontic vulnerability: whoever captures the custodian captures the record system. Taliban seizing U.S.-built biometric databases in Afghanistan, 2021. The same vulnerability in fiscal registries — budgets, treasury payments, procurement contracts — historically concentrated in a single ministry. Chrono-Ledger addresses a narrower failure: preservation of prior records should not depend on the continued control or survival of a single institution. This is why the architecture separates &lt;strong&gt;history&lt;/strong&gt; from &lt;strong&gt;authority&lt;/strong&gt; rather than pretending technology can replace sovereignty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Digital centralization.&lt;/strong&gt; Digitization moves vulnerability from archive rooms to software supply chains, credentials, and outsourced infrastructure. Equifax: 147 million records exposed. SolarWinds: compromised software supply chains reaching government networks. Chrono-Ledger requires confirmed history to be independently verifiable so that unauthorized retroactive modification becomes visible even when an infrastructure component is compromised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Distributed pilots: solve tampering, leave authority unresolved.&lt;/strong&gt; Georgia's Bitfury/NAPR project added blockchain timestamping without transferring authority over registry content. Sweden's Lantmäteriet pilot tested blockchain for property transactions but remained a pilot. Kazakhstan's tokenization sandbox operates under regulatory supervision. Konashevych's critique sharpens the lesson: these projects anchored hashes but did not make the blockchain itself the authoritative register. A hash proves a document hasn't changed — it cannot decide which title is valid, which court may rectify an entry, or how a rectification enters the canonical sequence. The real problem is institutional: who controls the sequence and who has authority to register, amend, rectify, or cancel entries.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Chrono-Ledger Architecture: Core Concept
&lt;/h2&gt;

&lt;p&gt;The four pathways require an architectural answer because each exploits the same coupling: the current legal status reflected in a register depends on record history, while preservation, verification, and correction remain institutionally concentrated.&lt;/p&gt;

&lt;p&gt;Chrono-Ledger Architecture binds the current legal status reflected in the register to verifiable history recorded on a public blockchain. Its simplest formulation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"The registry is the most recent state of its own archive."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Four defining conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every legally operative registry state derives from an ordered transaction history.&lt;/li&gt;
&lt;li&gt;Any participant can independently verify the public records, transitions, and past states that the law requires the registry to disclose.&lt;/li&gt;
&lt;li&gt;No single custodian can unilaterally rewrite the authoritative history.&lt;/li&gt;
&lt;li&gt;The relation between transaction history and current state is computationally reproducible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let &lt;strong&gt;Hₙ = (T₁, T₂, …, Tₙ)&lt;/strong&gt; denote the ordered transaction history after the n-th accepted transaction, where n is ledger-sequence position — not calendar time. In the simple case, the registered state is &lt;strong&gt;Sₙ = F(Hₙ)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For bitemporal accuracy, the model uses two time axes. Ledger-sequence position n and legal-effective time τ. The full formulation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S₍τ,n₎ = F(Hₙ, τ)&lt;/strong&gt; — the legal state at effective time τ, computed from the transaction history available after sequence position n.&lt;/p&gt;

&lt;p&gt;F records and reproduces the outcomes of legally authorized institutional acts. It does not replace their legal interpretation. If the history contains conflicting judicial or administrative acts and no competent institution has resolved the conflict, F returns &lt;strong&gt;disputed legal status&lt;/strong&gt; rather than selecting one act as controlling. The disputed status identifies the competing acts, their dates and jurisdictions, any appeal or suspension, and the competent institution empowered to resolve the conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correction Without Deletion
&lt;/h3&gt;

&lt;p&gt;An erroneous entry is not removed from history. A subsequent authorized corrective transaction supersedes it, and the current state is recomputed over the full sequence including the correction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T₁ → T₂ → T₃* → T₄ → C(T₃*) → … → Sₙ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The history remains immutable; the status reflected in the register changes through authorized acts. This distinguishes Chrono-Ledger from conventional databases, where correction often overwrites the record, and from naïve immutability, where errors become permanent law. The architecture makes the alterability of registered legal status compatible with the immutability of record history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrity vs. Validity
&lt;/h3&gt;

&lt;p&gt;The formula separates cryptographic integrity from legal truth. Chrono-Ledger guarantees the &lt;strong&gt;integrity&lt;/strong&gt; of the record history — entries were included as recorded and were not subsequently altered unnoticed. Existing legal and administrative institutions remain responsible for &lt;strong&gt;accuracy, authenticity, and legal validity&lt;/strong&gt;: they establish facts, identify authorized actors, and determine whether a transaction has legal effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three-Layer Separation
&lt;/h3&gt;

&lt;p&gt;The architecture rests on a three-layer separation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Record History&lt;/td&gt;
&lt;td&gt;Ordered, tamper-evident sequence of entries and changes&lt;/td&gt;
&lt;td&gt;Append-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current Legal Status&lt;/td&gt;
&lt;td&gt;Configuration of rights and obligations after giving effect to history&lt;/td&gt;
&lt;td&gt;Deterministically computable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Competent Authority&lt;/td&gt;
&lt;td&gt;Institution legally empowered to register, amend, rectify, cancel&lt;/td&gt;
&lt;td&gt;Identified in every transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Conventional registries collapse these layers into one institutional structure. Chrono-Ledger separates them: record history is append-only, the status reflected in the register changes through legally authorized acts, and the competent institution is identified in every transaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prior Art and Differentiation
&lt;/h3&gt;

&lt;p&gt;The closest antecedent in the academic blockchain-registry literature is Konashevych's cross-blockchain protocol for public registries (2020), which combines a standard database with several blockchains serving as immutable logs, while smart laws and digital authorities handle enforceability and correction. It shares three features with the present model: a blockchain-agnostic substrate, separation of transactional integrity from legal validity, and correction of erroneous entries through subsequent authorized entries.&lt;/p&gt;

&lt;p&gt;Chrono-Ledger differs in three respects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Archival theory as a design frame.&lt;/strong&gt; Konashevych treats the problem as data integrity and tokenization. Chrono-Ledger derives architectural requirements from the political theory of archives and frames the problem as one of state memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vulnerability typology.&lt;/strong&gt; The four pathways connect architectural choices to historically observed registry failures. Konashevych critiques existing pilots and proposes a cross-blockchain protocol, but does not organize registry failures into a vulnerability typology linking each architectural response to the failure mode it addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Institutional distribution of authority.&lt;/strong&gt; Konashevych develops smart laws and digital authorities. Chrono-Ledger isolates the institutional control over the canonical sequence as an independent architectural layer. The six invariants and the adversarial model have no counterpart in Konashevych's framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Implementation Models
&lt;/h2&gt;

&lt;p&gt;The paper compares Chrono-Ledger against conventional architectures using three cumulative tests: &lt;strong&gt;open verification&lt;/strong&gt;, &lt;strong&gt;consensus over one canonical sequence&lt;/strong&gt;, and &lt;strong&gt;validator membership independent of the institutions empowered to alter the register.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Open verification&lt;/th&gt;
&lt;th&gt;Canonical consensus&lt;/th&gt;
&lt;th&gt;Validator independence&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Centralized DB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Operator-selected&lt;/td&gt;
&lt;td&gt;No validator set&lt;/td&gt;
&lt;td&gt;Fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WORM archive&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No validator set&lt;/td&gt;
&lt;td&gt;Fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event sourcing&lt;/td&gt;
&lt;td&gt;Usually operator-defined&lt;/td&gt;
&lt;td&gt;Operator-selected&lt;/td&gt;
&lt;td&gt;No validator set&lt;/td&gt;
&lt;td&gt;Fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audited append-only DB&lt;/td&gt;
&lt;td&gt;Auditor-dependent&lt;/td&gt;
&lt;td&gt;Operator-selected&lt;/td&gt;
&lt;td&gt;No validator set&lt;/td&gt;
&lt;td&gt;Fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transparency log with witnesses&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No endogenous consensus&lt;/td&gt;
&lt;td&gt;Witness set separately governed&lt;/td&gt;
&lt;td&gt;Fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissioned BFT database&lt;/td&gt;
&lt;td&gt;Permission-dependent&lt;/td&gt;
&lt;td&gt;Yes, within admitted set&lt;/td&gt;
&lt;td&gt;No — membership predefined&lt;/td&gt;
&lt;td&gt;Fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State permissioned ledger&lt;/td&gt;
&lt;td&gt;Permission-dependent&lt;/td&gt;
&lt;td&gt;Yes, within admitted set&lt;/td&gt;
&lt;td&gt;No — state appoints validators&lt;/td&gt;
&lt;td&gt;Fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Public permissionless blockchain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Passes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only a public permissionless blockchain combines verification open to any participant, consensus over a single canonical sequence, and validation whose membership is not appointed by the institutions authorized to alter the register.&lt;/p&gt;

&lt;p&gt;Event sourcing reproduces state from history but leaves one operator as arbiter of the authoritative sequence. Transparency logs provide public inclusion and consistency proofs, but rely on separately governed logs, monitors, witnesses, or gossip to detect equivocation — they do not themselves establish one permissionless consensus over a canonical sequence. Permissioned BFT databases tolerate a bounded number of faulty nodes, but membership and ultimate control remain within a predefined administrative domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consortium Model
&lt;/h3&gt;

&lt;p&gt;A multi-institutional consortium model implements Chrono-Ledger with institutionally distributed authorization. Five roles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Power&lt;/th&gt;
&lt;th&gt;Cannot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Registry agencies&lt;/td&gt;
&lt;td&gt;Authorize ordinary transactions with institutional keys&lt;/td&gt;
&lt;td&gt;Control canonical history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Courts&lt;/td&gt;
&lt;td&gt;Authorize correction or annulment transactions&lt;/td&gt;
&lt;td&gt;Issue ordinary registrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Municipalities / local registrars&lt;/td&gt;
&lt;td&gt;Attest local facts&lt;/td&gt;
&lt;td&gt;Override court rulings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit office&lt;/td&gt;
&lt;td&gt;Oversee key rotation and procedural compliance&lt;/td&gt;
&lt;td&gt;Authorize transactions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observer nodes&lt;/td&gt;
&lt;td&gt;Independently verify public blockchain history&lt;/td&gt;
&lt;td&gt;Alter the ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No single role can authorize, submit, correct, and erase the same transaction. The public blockchain serves as the operational layer — every authorized transition is recorded on-chain, and under its consensus assumptions, its independent validator set makes alteration of the canonical record history publicly detectable without depending on any single registry institution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blockchain Substrate Selection
&lt;/h3&gt;

&lt;p&gt;Having established the canonical-layer requirements, selecting among qualifying public blockchains requires a second-stage assessment of six properties: distribution of validation power, finality, data availability, throughput, cost, and compatibility with the registry's disclosure rules.&lt;/p&gt;

&lt;p&gt;Specific considerations by network:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solana:&lt;/strong&gt; parallel execution + PoH/PoS design makes it a candidate for frequent transitions, but validator distribution must be assessed, not inferred from throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum rollups:&lt;/strong&gt; move execution from L1 while retaining settlement on-chain. Suitability depends on sequencer control, proof design, withdrawal latency. A rollup qualifies only if its complete canonical history remains reconstructible and no registry institution can control the accepted sequence through the sequencer or upgrade mechanism. Validium introduces off-chain data availability dependency under different security assumptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polkadot:&lt;/strong&gt; application-specific execution with shared security, but canonical history must live within the shared-security arrangement, not a separately controlled parachain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avalanche:&lt;/strong&gt; rapid probabilistic finality, but custom subnets qualify only when validator membership remains independent of registry institutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bitcoin:&lt;/strong&gt; slower probabilistic settlement with strong external anchoring — better suited to periodic commitments than high-frequency operational transitions. An anchor alone does not make the underlying registry a Chrono-Ledger if the complete authorized sequence remains under unilateral custodial control.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Architectural Invariants and Threat Model
&lt;/h2&gt;

&lt;p&gt;The architecture is stated as six operational invariants that translate the defining conditions into implementation requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Integrity.&lt;/strong&gt; For all n, Hₙ is append-only — any modification of a past transaction is detectable by any party with read access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open verification.&lt;/strong&gt; Any participant may verify the public records, transitions, and past states that the law requires the registry to disclose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproducibility of registered legal status.&lt;/strong&gt; Sₙ is the deterministic output of F applied to Hₙ. If Hₙ contains unresolved conflicting acts, F returns disputed status with references to every competing act. F does not select a prevailing claim. The status changes only when a competent institution issues a legally operative resolution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competent authority.&lt;/strong&gt; A transaction Tᵢ is admissible only if signed by an institutional key holder empowered under the applicable law and jurisdiction-specific rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correction transparency.&lt;/strong&gt; C(Tⱼ) does not delete Tⱼ from Hₙ. It appends, and Sₙ is recomputed over the full sequence including C(Tⱼ).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-exclusivity.&lt;/strong&gt; No single institutional role can both authorize a transaction and control the canonical record history.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chrono-Ledger therefore requires a sufficiently numerous and independently controlled validator set, such that no feasible colluding group can control the canonical sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adversarial Model
&lt;/h3&gt;

&lt;p&gt;The adversary may: (i) compromise an institutional authorization key; (ii) issue authorized but false transactions through oracle failure; or (iii) instruct a captured sovereign to abandon the chain for a rival fork.&lt;/p&gt;

&lt;p&gt;Key compromise can permit false but formally authorized entries until the key is revoked. The ledger preserves those entries and any subsequent correction as auditable evidence but cannot determine their factual truth. A large and independently controlled public validator set guarantees the integrity of the canonical record history by preventing any single registry institution from unilaterally rewriting the accepted sequence.&lt;/p&gt;

&lt;p&gt;In an authority fork, the architecture guarantees technical persistence but not legal continuity: a captured sovereign can fork the chain, while the pre-fork history survives as an independently verifiable record for transitional-justice or successor-state audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Sovereignty and the State
&lt;/h2&gt;

&lt;p&gt;Sovereignty, as the paper defines it for this argument, refers specifically to the state's legally recognized capacity to produce binding institutional decisions — the authority to determine what counts as a valid legal act — not to exclusive physical custody of records.&lt;/p&gt;

&lt;p&gt;The sovereignty objection is direct: if the state no longer controls the registry alone, has sovereignty been weakened?&lt;/p&gt;

&lt;p&gt;The answer: Chrono-Ledger reconfigures the state's role from &lt;strong&gt;custodian&lt;/strong&gt; of records to &lt;strong&gt;authorized participant&lt;/strong&gt; in a public blockchain protocol whose integrity is maintained by independent consensus. The state retains authority to determine which transactions are legally authorized; what it loses is unilateral power to alter authoritative history. This redistributes power within sovereignty rather than delegating sovereignty away.&lt;/p&gt;

&lt;p&gt;As Bodin and Jellinek established, sovereignty is a negative boundary — it defines what no external power may override, not a catalogue of operational tasks the state must perform itself. Chrono-Ledger follows this logic at the registry layer. The state does not acquire a new power of authorization. What changes is the record layer after authorization. Once a transaction is signed and submitted to a public chain whose consensus is maintained by independent validators, no single custodian — including the state itself — can silently alter or destroy the evidence of what was authorized. The sovereign function of determining legal validity is untouched; the custody function that enabled retrospective control over the record is redistributed.&lt;/p&gt;

&lt;p&gt;The most direct counterargument concerns the sovereign exception: if consensus rules prevent the state from altering a record during war, revolution, or catastrophic oracle failure, the protocol appears to determine when the rules may bend. The architecture's answer: the state may cease using the chain or fork it, but cannot thereby erase the independently verifiable pre-fork history. Sovereign action remains possible under more demanding conditions of accountability. Departure from the recorded sequence becomes a visible public act.&lt;/p&gt;

&lt;p&gt;Architectural trust does not replace institutional trust. Citizens who distrust a state have no automatic reason to trust a protocol written and operated by its contractors. Verifiability can nevertheless be relocated from particular custodians to procedures, allowing trust in the record to survive failures of trust in those custodians. Where both forms of trust fail together, the architecture protects the past as evidence but cannot restore the present as authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Conclusion
&lt;/h2&gt;

&lt;p&gt;Recording, preserving, and reproducing property rights is a constitutive function of the state. Law operates across time only to the extent that later institutions can establish what was decided earlier, by whom, and with what continuing effect. History shows that every record architecture — from the guarded document to the bureaucratic archive, from the centralized database to the DLT pilot — eliminates one vulnerability and opens another, because in each of them the record history, the legal status reproduced from it, and the authority to alter that status remain with the same party.&lt;/p&gt;

&lt;p&gt;The answer is architectural and reduces to three requirements. The current legal status must be reproduced from a replicated, cryptographically linked sequence of authorized transitions open to independent verification. Record history, legal status, and authority must be separated: the canonical sequence can and must be maintained by validators independent of the institutions empowered to alter the register, while the question of legal validity remains with the competent institutions under applicable law. Correction must take the form of a later authorized transaction, never the deletion of the past: history is immutable, status is alterable, and these two properties are compatible.&lt;/p&gt;

&lt;p&gt;The architecture's contribution stated precisely: blockchain does not produce truth, legality, or trust automatically — these remain the province of existing institutions. The architecture does something different: it renders the history of property-rights records non-rewritable by any single custodian, legal status recomputable from that history, and the authority to alter it recorded and auditable. Where an error enters the register, correction adds to the history rather than erasing from it.&lt;/p&gt;

&lt;p&gt;At the scale of state registries — land cadastres, corporate registrations, and identification systems — this would mark a different form of institutional resilience: state memory would no longer depend on a discrete archive but on a distributed, self-verifying foundation of legal reality. The design of that archive is part of the design of sovereignty.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Safarov, A. I., &amp;amp; Osovsky, M. (2026).&lt;/em&gt; From Archive to Architecture: Chrono-Ledger and the Institutional Logic of State Memory. &lt;em&gt;SSRN.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=7460243" rel="noopener noreferrer"&gt;Full paper&lt;/a&gt;. CC BY 4.0.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>architecture</category>
      <category>governance</category>
      <category>systems</category>
    </item>
    <item>
      <title>Hermes Agent Skills Hub: Why I Stopped Calling It a 'Prompt Marketplace'</title>
      <dc:creator>Azamat Safarov</dc:creator>
      <pubDate>Mon, 08 Jun 2026 10:41:12 +0000</pubDate>
      <link>https://dev.to/azamat_safarov/hermes-agent-skills-hub-why-i-stopped-calling-it-a-prompt-marketplace-48lp</link>
      <guid>https://dev.to/azamat_safarov/hermes-agent-skills-hub-why-i-stopped-calling-it-a-prompt-marketplace-48lp</guid>
      <description>&lt;h1&gt;
  
  
  🧩 Hermes Agent Skills Hub: Real CLI Tools, Not Prompts
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Why I'm Writing This
&lt;/h2&gt;

&lt;p&gt;I use &lt;strong&gt;TouchDesigner&lt;/strong&gt; — a nod-based environment from &lt;a href="https://derivative.ca/" rel="noopener noreferrer"&gt;Derivative&lt;/a&gt; where you build everything from nodes: CHOPs for data, SOPs for geometry, TOPs for textures, COMPs for components. You can do generative art, data visualization, VJ sets, interactive installations, real-time video processing. &lt;a href="https://derivative.ca/download" rel="noopener noreferrer"&gt;Download it free&lt;/a&gt; — the non-commercial license covers most use cases.&lt;/p&gt;

&lt;p&gt;I have &lt;code&gt;NewProject1.toe&lt;/code&gt; open on my desktop right now. &lt;code&gt;datto1&lt;/code&gt; CHOP, &lt;code&gt;hessen_geo&lt;/code&gt; geoCOMP holding a dot Sphere SOP, &lt;code&gt;render1&lt;/code&gt; and &lt;code&gt;cam1&lt;/code&gt; — a basic scene I spin up when testing ideas.&lt;/p&gt;

&lt;p&gt;I also use &lt;strong&gt;Hermes Agent&lt;/strong&gt; — an AI agent from &lt;strong&gt;Nous Research&lt;/strong&gt; that works through CLI and recently got a desktop version on Electron. I give it tasks, it executes them through terminal, browser, API. It has skills — extensions that add new tools.&lt;/p&gt;

&lt;p&gt;I realized something: &lt;strong&gt;TouchDesigner can visualize literally anything&lt;/strong&gt;. Feed it data — it draws. Feed it text — it renders. Feed it a 3D model — it shows it. The only question is how to get and process that data.&lt;/p&gt;

&lt;p&gt;Hermes can process text, code, web data, run scripts. But it can't draw. TouchDesigner can draw, but it can't think.&lt;/p&gt;

&lt;p&gt;So I thought: what if I connect them? Through &lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt;. So Hermes can send data to TouchDesigner, TouchDesigner can respond, and the whole thing works as one organism.&lt;/p&gt;

&lt;p&gt;This isn't just a nice idea. It's a practical need: I want my AI agent to visualize its work in real-time, right in TD. And I want to control TD from my Hermes chat.&lt;/p&gt;

&lt;p&gt;That's when &lt;strong&gt;Skills Hub&lt;/strong&gt; appeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Skills Hub Actually Is
&lt;/h2&gt;

&lt;p&gt;Nous Research launched &lt;strong&gt;Skills Hub&lt;/strong&gt; — a catalog of skills for Hermes Agent. I opened it and realized: this isn't a prompt catalog like I initially thought. These are &lt;strong&gt;real CLI tools&lt;/strong&gt; you can install and invoke directly from a session.&lt;/p&gt;

&lt;p&gt;Not "ask the agent to do something." Install a skill — and you get a new command.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers Are Messy (I Want to Be Honest)
&lt;/h2&gt;

&lt;p&gt;The original tweet from &lt;a href="https://x.com/Fluyeporlaweb/status/2062414219123765748" rel="noopener noreferrer"&gt;@Fluyeporlaweb&lt;/a&gt; says "89 built-in, 81 optional, 521 community." My own screenshot shows different numbers: 74 built-in, 95 optional, and 90,576 community entries (including third-party registries). The discrepancy probably means the tweet was posted after the screenshot was taken — the hub refreshes twice daily. I'm going with &lt;strong&gt;~691&lt;/strong&gt; as the working estimate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Screenshot&lt;/th&gt;
&lt;th&gt;Tweet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Ships with Hermes&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Install separately&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Community&lt;/td&gt;
&lt;td&gt;All registries&lt;/td&gt;
&lt;td&gt;90,576 (all)&lt;/td&gt;
&lt;td&gt;521 (curated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Registries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data sources&lt;/td&gt;
&lt;td&gt;Built-in + Optional + Anthropic + OpenAI + HuggingFace + NVIDIA + skills.sh (19,956) + ClawHub (69,471) + browse.sh (377) + LobeHub (505) + gstack (52)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What These Skills Actually Do
&lt;/h2&gt;

&lt;p&gt;Here are concrete examples I found on the first page:&lt;/p&gt;

&lt;h3&gt;
  
  
  macos-computer-use
&lt;/h3&gt;

&lt;p&gt;Takes desktop screenshots, moves mouse, types keys, scrolls. Runs in background without stealing focus. This is not "instructions for controlling macOS." It's a real headless UI driver. I've tested it — it actually clicks on my screen while I work in another window.&lt;/p&gt;

&lt;h3&gt;
  
  
  comfyui
&lt;/h3&gt;

&lt;p&gt;Generates images, video, and audio via ComfyUI from inside a Hermes session. Not "ask ComfyUI nicely" — actual pipe calls through CLI. I've installed it, it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  humanizer
&lt;/h3&gt;

&lt;p&gt;Strips passive voice, AI clichés, and generic phrasing from text. Returns text that doesn't look generated. I've used it for Telegram posts.&lt;/p&gt;

&lt;h3&gt;
  
  
  popular-web-designs
&lt;/h3&gt;

&lt;p&gt;54 real design systems (Stripe, Linear, Vercel, etc.) as deployable HTML/CSS templates. Not links to articles — actual code.&lt;/p&gt;

&lt;h3&gt;
  
  
  claude-code / codex
&lt;/h3&gt;

&lt;p&gt;Delegates coding tasks to Claude Code CLI and OpenAI Codex CLI respectively. Hermes doesn't try to write the code itself — it hands the task to the right specialist.&lt;/p&gt;

&lt;h3&gt;
  
  
  manim-video
&lt;/h3&gt;

&lt;p&gt;3Blue1Brown-style math/algorithm visualizations. These are Python scripts that render video.&lt;/p&gt;

&lt;h3&gt;
  
  
  excalidraw
&lt;/h3&gt;

&lt;p&gt;Hand-drawn architecture diagrams from text descriptions. I've used it for post diagrams.&lt;/p&gt;

&lt;h3&gt;
  
  
  ascii-video
&lt;/h3&gt;

&lt;p&gt;Converts any video to colored ASCII MP4 or GIF.&lt;/p&gt;

&lt;h3&gt;
  
  
  touchdesigner-mcp
&lt;/h3&gt;

&lt;p&gt;And this is the most interesting one for me. Controls a running TouchDesigner instance through MCP. Two channels: one for sending data to TD, one for receiving. This is exactly what I was thinking about at the start of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compatibility Part That Matters
&lt;/h2&gt;

&lt;p&gt;These aren't Hermes-only plugins. They work through the standard &lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt;, meaning you can also install them in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code&lt;/li&gt;
&lt;li&gt;Cursor&lt;/li&gt;
&lt;li&gt;Codex&lt;/li&gt;
&lt;li&gt;OpenCode&lt;/li&gt;
&lt;li&gt;Any MCP-compatible agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hermes is the coordinator, not the sole consumer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed for My TouchDesigner Workflow
&lt;/h2&gt;

&lt;p&gt;Before: I wrote a Python script outside TD, ran it, then manually moved the result into TD through DAT or CHOP.&lt;/p&gt;

&lt;p&gt;Now: I can tell Hermes "visualize this API data in TD", Hermes installs the right skills (or uses existing ones), data flows through MCP directly into TD — into a CHOP or TOP — and TD draws in real-time.&lt;/p&gt;

&lt;p&gt;Or the reverse: TD sends data (e.g., audio spectrum) through MCP to Hermes, Hermes analyzes and responds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Price
&lt;/h2&gt;

&lt;p&gt;Zero. Open source. No paywall, no token credits per call. The Hermes repo has &lt;strong&gt;186k stars and 31.9k forks&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hub Website Is Down
&lt;/h2&gt;

&lt;p&gt;The Skills Hub website (&lt;a href="https://hermes-agent.nousresearch.com/skills" rel="noopener noreferrer"&gt;hermes-agent.nousresearch.com/skills&lt;/a&gt;) is currently returning an error. I checked myself — right now. But the repository is fully active and &lt;code&gt;hermes skills install&lt;/code&gt; works from CLI. The website is a wrapper, not the product itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Skills Hub makes Hermes not just a "chatbot with terminal access" but a platform with an ecosystem. And for me, this is concrete: I can now connect my TouchDesigner with an AI agent through a standard protocol, without writing a single line of integration code.&lt;/p&gt;

&lt;p&gt;This is not a "prompt marketplace." This is a toolkit I can use right now.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://x.com/Fluyeporlaweb/status/2062414219123765748" rel="noopener noreferrer"&gt;X/Twitter post by @Fluyeporlaweb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TouchDesigner:&lt;/strong&gt; &lt;a href="https://derivative.ca/" rel="noopener noreferrer"&gt;derivative.ca&lt;/a&gt; | &lt;a href="https://derivative.ca/download" rel="noopener noreferrer"&gt;Download&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills Hub:&lt;/strong&gt; &lt;a href="https://hermes-agent.nousresearch.com/skills" rel="noopener noreferrer"&gt;hermes-agent.nousresearch.com/skills&lt;/a&gt; (site temporarily down)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hermes Docs:&lt;/strong&gt; &lt;a href="https://hermes-agent.nousresearch.com/docs" rel="noopener noreferrer"&gt;hermes-agent.nousresearch.com/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hermes Desktop:&lt;/strong&gt; &lt;a href="https://hermes-agent.nousresearch.com/desktop" rel="noopener noreferrer"&gt;hermes-agent.nousresearch.com/desktop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/NousResearch/hermes-agent" rel="noopener noreferrer"&gt;github.com/NousResearch/hermes-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Telegram:&lt;/strong&gt; &lt;a href="https://t.me/azamat_dasein" rel="noopener noreferrer"&gt;t.me/azamat_dasein&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hermesagent</category>
      <category>aiagents</category>
      <category>opensource</category>
      <category>touchdesigner</category>
    </item>
    <item>
      <title>Hermes Agent Burned 603M Tokens Behind My Back — I Cut Background Costs by Up to 125x</title>
      <dc:creator>Azamat Safarov</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:03:08 +0000</pubDate>
      <link>https://dev.to/azamat_safarov/hermes-agent-burned-603m-tokens-behind-my-back-i-cut-background-costs-by-up-to-125x-43oi</link>
      <guid>https://dev.to/azamat_safarov/hermes-agent-burned-603m-tokens-behind-my-back-i-cut-background-costs-by-up-to-125x-43oi</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.amazonaws.com%2Fuploads%2Farticles%2Fomw79knqbhrxwxwaqz80.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.amazonaws.com%2Fuploads%2Farticles%2Fomw79knqbhrxwxwaqz80.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last Tuesday I noticed my &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama Cloud Pro&lt;/a&gt; quota draining faster than usual. Way faster. I had burned through &lt;strong&gt;603 million tokens&lt;/strong&gt; in seven days without understanding where they went.&lt;/p&gt;

&lt;p&gt;I opened my &lt;a href="https://github.com/nousresearch/hermes-agent" rel="noopener noreferrer"&gt;Hermes Agent&lt;/a&gt; logs and found something I did not know existed: an &lt;code&gt;auxiliary:&lt;/code&gt; block with twelve background tasks. Compression, web extraction, vision, session search, skills matching — all running silently every time I typed a message. Every task was set to &lt;code&gt;provider: auto&lt;/code&gt;. And because I had no API keys for the fallback chain, every one silently fell back to &lt;code&gt;kimi-k2.6&lt;/code&gt;, my one-trillion-parameter main model.&lt;/p&gt;

&lt;p&gt;I had no idea this was happening. The agent was sending eleven background prompts to the same model I was actively chatting with, through the same quota, without showing me the prompts. Compression alone fired 10–20 times per long session, each pass sending the full conversation history.&lt;/p&gt;

&lt;p&gt;This is what I fixed immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Here is what I changed in the &lt;code&gt;auxiliary&lt;/code&gt; block of my &lt;code&gt;~/.hermes/config.yaml&lt;/code&gt;. The complete YAML is in the &lt;strong&gt;Full Config&lt;/strong&gt; section below.&lt;/p&gt;

&lt;p&gt;Apply with &lt;code&gt;/reset&lt;/code&gt; or restart Hermes. Config changes only take effect on new sessions.&lt;/p&gt;

&lt;p&gt;If you just want the config and don't care about the story, jump to &lt;strong&gt;Full Config&lt;/strong&gt; below, copy it, &lt;code&gt;/reset&lt;/code&gt;, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Routing Works
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fshxvazrv64ggackzpnzo.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.amazonaws.com%2Fuploads%2Farticles%2Fshxvazrv64ggackzpnzo.png" alt="Before and after: default provider auto vs optimized explicit routing" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Twelve tasks used to collapse into one trillion-parameter model. Now they are distributed across six models, from 8B to 1T.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;provider: auto&lt;/code&gt; Actually Does
&lt;/h2&gt;

&lt;p&gt;I searched dozens of Hermes guides online. Not a single one mentions the &lt;code&gt;auxiliary&lt;/code&gt; block. The official docs describe the YAML structure, but there's no warning that &lt;code&gt;provider: auto&lt;/code&gt; silently falls back to your main model. I only found one &lt;a href="https://www.youtube.com/watch?v=NoF-YajElIM&amp;amp;list=PLmpUb_PWAkDxewld5ZYyKifuHxgIbiq2d&amp;amp;index=14" rel="noopener noreferrer"&gt;video by AI Garage&lt;/a&gt; discussing this — nothing else. No blog posts, no Discord threads, no Reddit discussions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;auto&lt;/code&gt; chain is: &lt;code&gt;openrouter → new portal → codex → gemini flash&lt;/code&gt;. If none of these backends have an API key configured, it falls back to your &lt;strong&gt;main chat model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So while I was typing one message to k2.6, the agent was sending eleven others to the same model through the same quota, without showing me the prompt. Compression alone was firing 10–20 times per long session, sending the full conversation history every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Ollama Cloud Pro Catalog
&lt;/h2&gt;

&lt;p&gt;I have an Ollama Cloud Pro subscription. Here are the models available in my catalog that matter for routing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1T params, 256K context&lt;/td&gt;
&lt;td&gt;Reasoning, architecture, debugging&lt;/td&gt;
&lt;td&gt;Main chat only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1T params&lt;/td&gt;
&lt;td&gt;Same family, optimized for long context&lt;/td&gt;
&lt;td&gt;Summarization, compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3-vl:235b-instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;235B params&lt;/td&gt;
&lt;td&gt;Multimodal (vision + text)&lt;/td&gt;
&lt;td&gt;Screenshots, image analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~20B params&lt;/td&gt;
&lt;td&gt;Fast, good at structured output&lt;/td&gt;
&lt;td&gt;Safety checks, classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma3:12b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12B params&lt;/td&gt;
&lt;td&gt;Lightweight, fast&lt;/td&gt;
&lt;td&gt;Triage, profile tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rnj-1:8b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8B params&lt;/td&gt;
&lt;td&gt;Cheapest in catalog&lt;/td&gt;
&lt;td&gt;Titles, search, skills matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma4:e2b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2B params&lt;/td&gt;
&lt;td&gt;Smallest&lt;/td&gt;
&lt;td&gt;Not used — too weak for any auxiliary task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I pulled all of these and tested them against the twelve auxiliary tasks. The routing below is the result of that testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twelve Background Tasks
&lt;/h2&gt;

&lt;p&gt;My Hermes version has twelve auxiliary tasks. I ordered them by how much they cost in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it costs money&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;compression&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Summarizes conversation when context exceeds limits&lt;/td&gt;
&lt;td&gt;Fires 10–20 times per long session. Each pass sends the full conversation history.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;web_extract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strips HTML boilerplate after &lt;code&gt;web_search&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fires every time you search the web.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vision&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Processes screenshots and images&lt;/td&gt;
&lt;td&gt;Multimodal tokens are expensive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;flush_memories&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes facts to memory files on &lt;code&gt;/new&lt;/code&gt; or &lt;code&gt;/exit&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Runs at every session end.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kanban_decomposer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Breaks Kanban tasks into steps&lt;/td&gt;
&lt;td&gt;Medium complexity, runs on board operations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Analyzes skill quality and redundancy&lt;/td&gt;
&lt;td&gt;Heavy analysis task.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;session_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Searches past sessions and summarizes matches&lt;/td&gt;
&lt;td&gt;Runs when you look up old conversations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;skills_hub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Matches your query to installed skills&lt;/td&gt;
&lt;td&gt;Runs on most questions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;triage_specifier&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Classifies incoming messages&lt;/td&gt;
&lt;td&gt;Binary classification.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;approval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Binary safety check before terminal commands&lt;/td&gt;
&lt;td&gt;Simple yes/no on safety.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;code&gt;profile_describer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generates profile bio&lt;/td&gt;
&lt;td&gt;Rare, lightweight.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;code&gt;title_generation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto-names new sessions&lt;/td&gt;
&lt;td&gt;Trivial, runs constantly.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note: older Hermes versions (like the one in the AI Garage video below) show eight tasks. My version has twelve — four were added in recent updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Mapped Models to Tasks
&lt;/h2&gt;

&lt;p&gt;The logic is dead simple: put the lightest model that doesn't break on each task, and keep k2.6 for actual conversations.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Why this one&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Main chat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Architecture, debugging, discussion. The only task that actually needs a trillion parameters.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compression, web_extract, kanban, curator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same &lt;a href="https://www.moonshot.cn" rel="noopener noreferrer"&gt;Kimi&lt;/a&gt; family, optimized for long context. Summarization quality stays high. Using k2.6 for compression was burning quota for no quality gain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vision&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen3-vl:235b-instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only multimodal model in the catalog at this level. No alternative exists for image analysis.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Triage, profile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma3:12b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12B params vs 1T. Classification and bio generation do not need reasoning depth.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Approval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deepseek-v4-flash&lt;/code&gt; (~20B)&lt;/td&gt;
&lt;td&gt;Binary safety check. Fast response time matters more than reasoning quality.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Titles, search, skills, MCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rnj-1:8b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8B params. 125 times lighter than k2.6. The bulk of the savings are here — these tasks run constantly but need minimal intelligence.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I Tried First: Local Models
&lt;/h2&gt;

&lt;p&gt;Before settling on cloud routing, I tried running auxiliary tasks locally. I already had &lt;code&gt;gemma4:e2b&lt;/code&gt; pulled via Ollama on my machine.&lt;/p&gt;

&lt;p&gt;RTX 5070 Ti, 8 GB VRAM. One 6B-parameter model fits. Two is already borderline. Every time Hermes switched from compression to approval, Ollama unloaded one model and loaded another. Five to ten seconds of dead air. The GPU fan kicked in. I lost more time waiting for model swaps than I saved on tokens. I abandoned local auxiliary models the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;Here is what the routing actually means in terms of model size:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Before (default)&lt;/th&gt;
&lt;th&gt;After (routed)&lt;/th&gt;
&lt;th&gt;Reduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Titles, search, skills, MCP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rnj-1:8b&lt;/code&gt; (8B)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;125x lighter&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triage, profile&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gemma3:12b&lt;/code&gt; (12B)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;83x lighter&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deepseek-v4-flash&lt;/code&gt; (~20B)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50x lighter&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression, web_extract&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.5&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;Same family, frees k2.6 for chat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;qwen3-vl&lt;/code&gt; (235B)&lt;/td&gt;
&lt;td&gt;Dedicated multimodal model&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=NoF-YajElIM&amp;amp;list=PLmpUb_PWAkDxewld5ZYyKifuHxgIbiq2d&amp;amp;index=14" rel="noopener noreferrer"&gt;The video by AI Garage&lt;/a&gt; measured compression cost directly: Claude Opus at 50K context = 13 cents per pass. Kimi K2 for the same task = 1.9 cents. That is an 85% reduction for a single compression pass. Compression fires 10–20 times per day for heavy users. The author estimated that with default settings, compression alone can cost $60/month with Claude Opus. Routed to a cheaper model, it drops to $9/month.&lt;/p&gt;

&lt;p&gt;I cannot confirm exact dollar savings for Ollama Cloud — they do not expose per-call pricing. But the scale difference is unambiguous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heavy auxiliary on k2.5&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Compression and web_extract no longer block the main model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision on qwen3-vl&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Only multimodal option available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium tasks on gemma3:12b&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Triage and profile classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval on deepseek-v4-flash&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Fast binary decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light tasks on rnj-1:8b&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Titles, search, skills, MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;provider: auto&lt;/code&gt; removed&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;td&gt;Explicit provider on every task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Ollama auxiliary&lt;/td&gt;
&lt;td&gt;Abandoned&lt;/td&gt;
&lt;td&gt;VRAM contention on 8 GB laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost tracking per task&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Ollama Cloud does not expose per-call pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sessions no longer pause for &lt;code&gt;Compressing messages&lt;/code&gt;. The token counter stopped monopolizing k2.6. Quota exhaustion mid-session is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Config
&lt;/h2&gt;

&lt;p&gt;Here is the complete &lt;code&gt;auxiliary:&lt;/code&gt; block from my &lt;code&gt;~/.hermes/config.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;auxiliary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kimi-k2.5&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
  &lt;span class="na"&gt;web_extract&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kimi-k2.5&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;360&lt;/span&gt;
  &lt;span class="na"&gt;kanban_decomposer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kimi-k2.5&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;180&lt;/span&gt;
  &lt;span class="na"&gt;curator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kimi-k2.5&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;
  &lt;span class="na"&gt;vision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qwen3-vl:235b-instruct&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
    &lt;span class="na"&gt;download_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;triage_specifier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemma3:12b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
  &lt;span class="na"&gt;profile_describer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemma3:12b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
  &lt;span class="na"&gt;approval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deepseek-v4-flash&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;title_generation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rnj-1:8b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;session_search&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rnj-1:8b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="na"&gt;max_concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;skills_hub&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rnj-1:8b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rnj-1:8b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you run Hermes Agent and have never touched the auxiliary block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes config edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find &lt;code&gt;auxiliary:&lt;/code&gt;. Set explicit &lt;code&gt;provider&lt;/code&gt; and &lt;code&gt;model&lt;/code&gt; for every task — the one that handles it without dragging extra parameters. Save. Run &lt;code&gt;/reset&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then tomorrow run &lt;code&gt;hermes insights --days 1&lt;/code&gt;. Your main model should stop eating the entire token budget.&lt;/p&gt;

&lt;p&gt;If you use Claude or another frontier model as your main provider, the default config is even more expensive — every background task inherits that model. Route them to smaller models. Or go local if your hardware handles it.&lt;/p&gt;

&lt;p&gt;What auxiliary routing are you using? Drop it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt; — &lt;a href="https://www.linkedin.com/in/azamat-safarov-4a37b93a7/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/azamat-safarov-4a37b93a7/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;X / Twitter&lt;/strong&gt; — &lt;a href="https://x.com/Azamat__Safarov" rel="noopener noreferrer"&gt;https://x.com/Azamat__Safarov&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hermes</category>
      <category>ollama</category>
      <category>optimization</category>
      <category>tokencost</category>
    </item>
    <item>
      <title>Automating NotebookLM with Hermes Agent: From Research to Multi-Platform Content</title>
      <dc:creator>Azamat Safarov</dc:creator>
      <pubDate>Wed, 27 May 2026 13:23:16 +0000</pubDate>
      <link>https://dev.to/azamat_safarov/automating-notebooklm-with-hermes-agent-from-research-to-multi-platform-content-2k2h</link>
      <guid>https://dev.to/azamat_safarov/automating-notebooklm-with-hermes-agent-from-research-to-multi-platform-content-2k2h</guid>
      <description>&lt;h1&gt;
  
  
  Automating NotebookLM with Hermes Agent: From Research to Multi-Platform Content
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;I write every day. NotebookLM made the writing smarter — analyzing sources, generating podcasts, building visuals. But getting those artifacts out of Google and into my publishing pipeline still took 30 minutes of manual clicking and downloading. I fixed that by wiring NotebookLM directly into Hermes Agent. Now research turns into publish-ready content in about two minutes.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem I Was Solving
&lt;/h2&gt;

&lt;p&gt;NotebookLM is one of the best research tools Google has shipped. Drop in a PDF, a few URLs, or a research query and it builds a notebook — a living summary you can chat with, quiz, or turn into artifacts.&lt;/p&gt;

&lt;p&gt;But here is where it breaks for daily publishing. After NotebookLM generates a podcast, a mind map, or a slide deck, you are back to manual mode. Visit the site. Click each artifact. Download. Rename. Move to the right folder. Format for the platform you are posting to. I was spending nearly half an hour on this for every article. The AI did the creative work; I did the file management.&lt;/p&gt;

&lt;p&gt;Worse, the standard approach for automating this — browser automation via Playwright — hits a wall. Google detects headless Chromium and Firefox instantly. The login page throws "browser not secure" and you are done. Every automation script I tried failed at authentication.&lt;/p&gt;

&lt;p&gt;I needed a way to run NotebookLM from code, download artifacts programmatically, and route them into my publishing pipeline. Hermes Agent already handled the platform distribution. I just needed the bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cookie Export Hack That Works
&lt;/h2&gt;

&lt;p&gt;The only authentication method that survived Google's bot detection was exporting cookies from my real Chrome browser. No headless browsers, no Puppeteer tricks. Just real cookies from a real session.&lt;/p&gt;

&lt;p&gt;Here is what works reliably:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into &lt;code&gt;notebooklm.google.com&lt;/code&gt; in your normal Chrome or Edge&lt;/li&gt;
&lt;li&gt;Export cookies for domains &lt;code&gt;google.com&lt;/code&gt; and &lt;code&gt;notebooklm.google.com&lt;/code&gt; as Netscape format using any cookie-export extension&lt;/li&gt;
&lt;li&gt;Convert that text file to Playwright's &lt;code&gt;storage_state.json&lt;/code&gt; format&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;notebooklm-py&lt;/code&gt; wrapper reads that state file and gets a valid token
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# convert_netscape_cookies.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;

&lt;span class="n"&gt;netscape_lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="nf"&gt;readlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cookies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;netscape_lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpOnly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HttpOnly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httponly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Secure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sameSite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cookies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;origins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;storage_state.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run this once per session. 23 cookies are enough for full API access. Until Google rotates their auth, this is the only reliable path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The NotebookLM CLI That Does Everything
&lt;/h2&gt;

&lt;p&gt;Once authenticated, &lt;code&gt;notebooklm-py&lt;/code&gt; exposes a clean CLI and Python API. I wrapped this into Hermes skills so the agent can create notebooks, add sources, generate artifacts, and poll for completion without me touching the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Notebook and Adding Sources
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm create &lt;span class="s2"&gt;"Philosophy Notes"&lt;/span&gt;
&lt;span class="c"&gt;# → notebook ID returned&lt;/span&gt;

python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm &lt;span class="nb"&gt;source &lt;/span&gt;add &amp;lt;notebook-id&amp;gt; https://example.com/article.pdf
python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm &lt;span class="nb"&gt;source &lt;/span&gt;add-research &amp;lt;notebook-id&amp;gt; &lt;span class="s2"&gt;"Stoicism in modern productivity"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sources can be URLs, local files, Google Drive URLs, or research queries. NotebookLM processes them and builds an indexed knowledge base you can chat with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating Artifacts
&lt;/h3&gt;

&lt;p&gt;Each artifact has its own command and output format.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Audio Overview&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate audio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MP3&lt;/td&gt;
&lt;td&gt;Podcasts, Telegram voice messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infographic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate infographic "cover..."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;Article covers, diagrams, flowcharts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mind Map&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate mind-map --instructions "..."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Architecture overviews, concept maps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slide Deck&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate slide-deck "5 slides..."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;Presentations, LinkedIn carousels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quiz&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate quiz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;td&gt;Telegram polls, engagement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the positional argument for prompts — &lt;code&gt;--prompt&lt;/code&gt; does not exist. Most artifacts accept the description as a positional string.&lt;/p&gt;

&lt;p&gt;One catch: &lt;code&gt;mind-map&lt;/code&gt; does not accept &lt;code&gt;--style&lt;/code&gt; or &lt;code&gt;--orientation&lt;/code&gt;. Only &lt;code&gt;--instructions&lt;/code&gt;. And &lt;code&gt;--wait&lt;/code&gt; can time out on slow generations, so I poll separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Launch generation without waiting&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm generate infographic &lt;span class="s2"&gt;"Cover image for article about..."&lt;/span&gt; &lt;span class="nt"&gt;--orientation&lt;/span&gt; landscape &lt;span class="nt"&gt;--style&lt;/span&gt; professional &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="c"&gt;# → {"task_id": "...", "status": "pending"}&lt;/span&gt;

&lt;span class="c"&gt;# Poll until done&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm artifact poll &amp;lt;task_id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the status flips to &lt;code&gt;completed&lt;/code&gt;, a Google Cloud Storage URL is returned. I download that asset and route it to the right platform folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring It Into the Publishing Pipeline
&lt;/h2&gt;

&lt;p&gt;The full flow in Hermes Agent looks like this:&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%2Fraw.githubusercontent.com%2FAzamatSafarov%2Fcreative-system%2Fmain%2Fassets%2Fnotebooklm-hermes-diagram.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%2Fraw.githubusercontent.com%2FAzamatSafarov%2Fcreative-system%2Fmain%2Fassets%2Fnotebooklm-hermes-diagram.png" alt="Diagram: Hermes Agent pipeline" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source material (markdown, PDFs, URLs)
        ↓
NotebookLM notebook created
        ↓
Sources added → NotebookLM indexes and summarizes
        ↓
Artifacts generated (infographic, mind-map, audio, slides)
        ↓
Hermes downloads, compresses, renames assets
        ↓
Platform-specific routing:
   Dev.to      → full article with embedded infographic
   Telegram    → voice message (audio compressed to 48kbps mono)
   LinkedIn    → carousel from slide deck PDF
   Medium      → essay with timeline / quote cards
   Bluesky     → dense teaser + link to full article
   Mastodon    → threaded summary with cover image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole sequence runs as a single Hermes skill. I trigger it with one line: &lt;code&gt;generate notebooklm article for &amp;lt;topic&amp;gt;&lt;/code&gt;. The agent creates the notebook, waits for artifacts, downloads them, compresses audio for Telegram's 20 MB limit, and writes platform drafts in parallel.&lt;/p&gt;

&lt;p&gt;No more clicking through Google UI. No more drag-and-drop. The AI generates; the pipeline publishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Working Integration Script
&lt;/h2&gt;

&lt;p&gt;Here is the core pattern I use inside the Hermes skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;notebooklm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NotebookLMClient&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NotebookLMClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Create notebook
&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_notebook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stoic Productivity Analysis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Add sources — mix of URLs and research queries
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/seneca-essay.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_research&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;modern applications of Stoic principles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Generate cover image (infographic)
&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_artifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;artifact_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infographic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dark-themed cover image for a technical article about AI-powered research tools. Ocean color palette.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 4. Poll for completion
&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll_artifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 5. Download
&lt;/span&gt;&lt;span class="n"&gt;cover_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cover_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assets/generated/cover.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 6. Generate audio podcast
&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_artifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifact_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;audio_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;
&lt;span class="c1"&gt;# ... same poll pattern ...
&lt;/span&gt;&lt;span class="n"&gt;audio_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll_artifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_task&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assets/generated/podcast.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 7. Compress for Telegram (20 MB limit)
&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffmpeg -i assets/generated/podcast.mp3 -b:a 48k -ac 1 assets/generated/podcast_tg.mp3&lt;/span&gt;&lt;span class="sh"&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 &lt;code&gt;NotebookLMClient&lt;/code&gt; reads the same &lt;code&gt;storage_state.json&lt;/code&gt; I built from Chrome cookies. No login step, no browser puppetry. Just API calls against a valid Google session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Fits in the Larger Stack
&lt;/h2&gt;

&lt;p&gt;NotebookLM is not where content starts — it is where research gets processed. My pipeline already had source material (markdown drafts, PDFs, web pages). NotebookLM adds three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audio Overview&lt;/strong&gt; — two-voice podcast that I can drop straight into Telegram as a voice message. Compression takes a 30 MB raw file down to 7-10 MB. Voices stay clear at 48 kbps mono.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Infographics&lt;/strong&gt; — article covers and process diagrams generated from a prompt. Before this I used Python Pillow locally. It worked, but every cover looked like the same template. NotebookLM generates unique styles per article. I just describe what I want.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mind Maps&lt;/strong&gt; — architecture and concept maps that I embed in Dev.to and Medium articles. The output comes as JSON with node coordinates, so I can render it or let NotebookLM produce the visual.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I still write the original text. I still decide the angle. But the repetitive work — cover design, audio production, diagram drawing — moved from my desk to the agent's queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke Along the Way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Playwright login is dead.&lt;/strong&gt; Every Chromium and Firefox instance I tried was detected. "Browser not secure" at the Google login screen, every time. The cookie export method is the only survivor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong prompt syntax.&lt;/strong&gt; I spent an hour debugging &lt;code&gt;generate infographic --prompt "..."&lt;/code&gt; before realizing the flag does not exist. Positional arguments only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio timing.&lt;/strong&gt; NotebookLM audio generation takes 5-10 minutes. If I use &lt;code&gt;--wait&lt;/code&gt;, the CLI sometimes times out after 60 seconds. I now always launch with &lt;code&gt;--json&lt;/code&gt; to get the task ID, then poll in a loop until &lt;code&gt;completed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image size limits.&lt;/strong&gt; Bluesky caps images at 2 MB. Raw NotebookLM PNGs are often 2.3-2.8 MB. I run them through &lt;code&gt;ffmpeg&lt;/code&gt; for JPEG compression at 800px width before uploading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results After Running This
&lt;/h2&gt;

&lt;p&gt;Content production time dropped from research-plus-design-plus-publishing to research-plus-two-minutes. The bottleneck is no longer tooling — it is deciding what to write. NotebookLM handles visuals and audio. Hermes handles routing and platform formatting. I handle ideas.&lt;/p&gt;

&lt;p&gt;The full pipeline code and architecture diagrams are in the repository linked below. If you are already using Hermes Agent, the &lt;code&gt;notebooklm&lt;/code&gt; skill slots in with two CLI commands and a cookie file.&lt;/p&gt;

&lt;p&gt;This whole pipeline runs on the &lt;strong&gt;LLM Wiki&lt;/strong&gt; pattern by Andrej Karpathy — a persistent wiki that the LLM builds and maintains as you go, instead of RAG that re-discovers everything from scratch on every query.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f#file-llm-wiki-md" rel="noopener noreferrer"&gt;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f#file-llm-wiki-md&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Dev.to&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://dev.to/azamat_safarov_119e17602f/"&gt;https://dev.to/azamat_safarov_119e17602f/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://medium.com/@akutagorasava777" rel="noopener noreferrer"&gt;https://medium.com/@akutagorasava777&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paragraph&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://paragraph.com/@azamatsafarov/how-i-cut-my-publishing-time-from-thirty-minutes-to-two" rel="noopener noreferrer"&gt;https://paragraph.com/@azamatsafarov/how-i-cut-my-publishing-time-from-thirty-minutes-to-two&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;X&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://x.com/Azamat__Safarov" rel="noopener noreferrer"&gt;https://x.com/Azamat__Safarov&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>notebooklm</category>
      <category>hermes</category>
    </item>
    <item>
      <title>Automating NotebookLM with Hermes Agent: From Research to Multi-Platform Content</title>
      <dc:creator>Azamat Safarov</dc:creator>
      <pubDate>Wed, 27 May 2026 12:22:13 +0000</pubDate>
      <link>https://dev.to/azamat_safarov/automating-notebooklm-with-hermes-agent-from-research-to-multi-platform-content-15fo</link>
      <guid>https://dev.to/azamat_safarov/automating-notebooklm-with-hermes-agent-from-research-to-multi-platform-content-15fo</guid>
      <description>&lt;h1&gt;
  
  
  Automating NotebookLM with Hermes Agent: From Research to Multi-Platform Content
&lt;/h1&gt;




&lt;p&gt;NotebookLM made the writing smarter — analyzing sources, generating podcasts, building visuals. But getting those artifacts out of Google and into my publishing pipeline still took 30 minutes of manual clicking and downloading. I fixed that by wiring NotebookLM directly into Hermes Agent. Now research turns into publish-ready content in about two minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Was Mechanical Work
&lt;/h2&gt;

&lt;p&gt;I would finish an article, open NotebookLM, upload sources, wait for generation, then manually download each artifact. Podcast for Telegram. Infographic for cover image. Mind map for architecture section. Each download meant clicking through Google UI, renaming files, moving to assets folder, compressing for platform limits. Half an hour of file management for every article. The AI did the creative work; I did the clicks.&lt;/p&gt;

&lt;p&gt;Worse, browser automation failed. Google detects headless Chromium instantly. The login page throws "browser not secure" and blocks every script. Puppeteer, Playwright, Selenium — all detected. I needed a way to run NotebookLM from code without fighting Google's bot detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Cookie Export
&lt;/h2&gt;

&lt;p&gt;The breakthrough was cookie export. Real Chrome session, real cookies, no headless browser. I log into notebooklm.google.com normally, export cookies as Netscape format using a Chrome extension, convert to Playwright storage state with a Python script. The notebooklm-py wrapper reads that state and gets a valid token. Twenty-three cookies are enough for full API access.&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.amazonaws.com%2Fuploads%2Farticles%2Fuwsf7jzdi8ebp602x88c.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.amazonaws.com%2Fuploads%2Farticles%2Fuwsf7jzdi8ebp602x88c.png" alt="Architecture Diagram" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The CLI Workflow
&lt;/h2&gt;

&lt;p&gt;Once authenticated, the CLI exposes clean Python API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;notebooklm_cli&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NotebookLMClient&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NotebookLMClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storage_state_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;notebook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Research Topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;source_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notebook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources/my-article.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_infographic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cinematic visualization&lt;/span&gt;&lt;span class="sh"&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 gotcha is timing. Audio generation takes five to ten minutes. The positional argument syntax matters — infographic and slide-deck take prompts as positionals, mind-map only accepts --instructions, audio builds from sources directly.&lt;/p&gt;

&lt;p&gt;Key commands that work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create and use notebook&lt;/span&gt;
notebooklm create &lt;span class="s2"&gt;"My Research"&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
notebooklm use &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;# Add sources&lt;/span&gt;
notebooklm &lt;span class="nb"&gt;source &lt;/span&gt;add article.md

&lt;span class="c"&gt;# Generate artifacts (positional args)&lt;/span&gt;
notebooklm generate infographic &lt;span class="s2"&gt;"Visual description"&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
notebooklm generate audio &lt;span class="s2"&gt;"Podcast"&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; deep-dive

&lt;span class="c"&gt;# Poll manually for audio&lt;/span&gt;
notebooklm artifact poll &amp;lt;task_id&amp;gt;
notebooklm download artifact.mp3 &lt;span class="nt"&gt;--latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Integration with Hermes Agent
&lt;/h2&gt;

&lt;p&gt;The full pipeline is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Hermes Agent skill calls
&lt;/span&gt;&lt;span class="n"&gt;hermes&lt;/span&gt; &lt;span class="n"&gt;notebooklm&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;bridge&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;research&lt;/span&gt; \
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;articles/*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cover,podcast,mindmap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;compress&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;platforms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes it runs: create notebook, add sources, generate cover infographic, poll for completion, download, compress audio for Telegram's 20MB limit, write platform drafts in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke Along The Way
&lt;/h2&gt;

&lt;p&gt;Playwright login is dead for NotebookLM — every Chromium instance detected by Google's bot protection.&lt;/p&gt;

&lt;p&gt;Wrong prompt syntax cost an hour debugging --prompt before realizing positional arguments only for most commands.&lt;/p&gt;

&lt;p&gt;Audio timing taught me to poll separately instead of waiting — --wait times out after 60 seconds.&lt;/p&gt;

&lt;p&gt;Image size limits for Bluesky's 2MB meant ffmpeg compression before upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Content production dropped from research plus 30 minutes manual to research plus 2 minutes automated. NotebookLM handles visuals and audio. Hermes handles routing and platform formatting. I handle ideas. The bottleneck is no longer tooling — it is deciding what to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;The full pipeline is open source: &lt;a href="https://github.com/AzamatSafarov/hermes-notebooklm-bridge" rel="noopener noreferrer"&gt;https://github.com/AzamatSafarov/hermes-notebooklm-bridge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This pipeline runs on the LLM Wiki pattern by Andrej Karpathy — persistent wiki maintained by the LLM as you go, instead of RAG that re-discovers everything from scratch on every query.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #python #automation #notebooklm #hermes #content-pipeline #ai-tools&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>notebooklm</category>
      <category>hermes</category>
    </item>
    <item>
      <title>Automating NotebookLM with Hermes Agent: From Research to Multi-Platform Content</title>
      <dc:creator>Azamat Safarov</dc:creator>
      <pubDate>Tue, 26 May 2026 10:38:27 +0000</pubDate>
      <link>https://dev.to/azamat_safarov/automating-notebooklm-with-hermes-agent-from-research-to-multi-platform-content-4o1l</link>
      <guid>https://dev.to/azamat_safarov/automating-notebooklm-with-hermes-agent-from-research-to-multi-platform-content-4o1l</guid>
      <description>&lt;p&gt;&lt;em&gt;NotebookLM made the writing smarter — analyzing sources, generating podcasts, building visuals. But getting those artifacts out of Google and into my publishing pipeline still took 30 minutes of manual clicking and downloading. I fixed that by wiring NotebookLM directly into Hermes Agent. Now research turns into publish-ready content in about two minutes.&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.amazonaws.com%2Fuploads%2Farticles%2Fwq7fqwtu9wqaz0sbp1jk.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.amazonaws.com%2Fuploads%2Farticles%2Fwq7fqwtu9wqaz0sbp1jk.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem I Was Solving
&lt;/h2&gt;

&lt;p&gt;NotebookLM is one of the best research tools Google has shipped. Drop in a PDF, a few URLs, or a research query and it builds a notebook — a living summary you can chat with, quiz, or turn into artifacts.&lt;/p&gt;

&lt;p&gt;But here is where it breaks for daily publishing. After NotebookLM generates a podcast, a mind map, or a slide deck, you are back to manual mode. Visit the site. Click each artifact. Download. Rename. Move to the right folder. Format for the platform you are posting to. I was spending nearly half an hour on this for every article. The AI did the creative work; I did the file management.&lt;/p&gt;

&lt;p&gt;Worse, the standard approach for automating this — browser automation via Playwright — hits a wall. Google detects headless Chromium and Firefox instantly. The login page throws "browser not secure" and you are done. Every automation script I tried failed at authentication.&lt;/p&gt;

&lt;p&gt;I needed a way to run NotebookLM from code, download artifacts programmatically, and route them into my publishing pipeline. Hermes Agent already handled the platform distribution. I just needed the bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cookie Export Hack That Works
&lt;/h2&gt;

&lt;p&gt;The only authentication method that survived Google's bot detection was exporting cookies from my real Chrome browser. No headless browsers, no Puppeteer tricks. Just real cookies from a real session.&lt;/p&gt;

&lt;p&gt;Here is what works reliably:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into &lt;code&gt;notebooklm.google.com&lt;/code&gt; in your normal Chrome or Edge&lt;/li&gt;
&lt;li&gt;Export cookies for domains &lt;code&gt;google.com&lt;/code&gt; and &lt;code&gt;notebooklm.google.com&lt;/code&gt; as Netscape format using any cookie-export extension&lt;/li&gt;
&lt;li&gt;Convert that text file to Playwright's &lt;code&gt;storage_state.json&lt;/code&gt; format&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;notebooklm-py&lt;/code&gt; wrapper reads that state file and gets a valid token
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# convert_netscape_cookies.py — one-time setup, reusable
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;

&lt;span class="n"&gt;netscape_lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="nf"&gt;readlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cookies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;netscape_lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpOnly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HttpOnly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httponly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Secure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sameSite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cookies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;origins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;storage_state.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Converted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cookies, saved to storage_state.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage: &lt;code&gt;python convert_netscape_cookies.py exported_cookies.txt&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I run this once per session. 23 cookies are enough for full API access. Until Google rotates their auth, this is the only reliable path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The NotebookLM CLI That Does Everything
&lt;/h2&gt;

&lt;p&gt;Once authenticated, &lt;code&gt;notebooklm-py&lt;/code&gt; exposes a clean CLI and Python API. I wrapped this into Hermes skills so the agent can create notebooks, add sources, generate artifacts, and poll for completion without me touching the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Notebook and Adding Sources
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a notebook&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm create &lt;span class="s2"&gt;"Philosophy Notes"&lt;/span&gt;
&lt;span class="c"&gt;# → returns notebook ID&lt;/span&gt;

&lt;span class="c"&gt;# Add sources — URLs, files, research queries&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm &lt;span class="nb"&gt;source &lt;/span&gt;add &amp;lt;notebook-id&amp;gt; https://example.com/article.pdf
python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm &lt;span class="nb"&gt;source &lt;/span&gt;add-research &amp;lt;notebook-id&amp;gt; &lt;span class="s2"&gt;"Stoicism in modern productivity"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sources can be URLs, local files, Google Drive URLs, or research queries. NotebookLM processes them and builds an indexed knowledge base you can chat with.&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.amazonaws.com%2Fuploads%2Farticles%2Fgh67141syhboqfz5mkv6.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.amazonaws.com%2Fuploads%2Farticles%2Fgh67141syhboqfz5mkv6.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating Artifacts
&lt;/h3&gt;

&lt;p&gt;Each artifact has its own command and output format.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Audio Overview&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate audio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MP3&lt;/td&gt;
&lt;td&gt;Podcasts, Telegram voice messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infographic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate infographic "cover..."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;Article covers, diagrams, flowcharts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mind Map&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate mind-map --instructions "..."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Architecture overviews, concept maps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slide Deck&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate slide-deck "5 slides..."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;Presentations, LinkedIn carousels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quiz&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate quiz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;td&gt;Telegram polls, engagement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the positional argument for prompts. &lt;code&gt;--prompt&lt;/code&gt; does not exist for most artifacts. Infographic, slide-deck, and report accept the description as a positional string. Mind-map only accepts &lt;code&gt;--instructions&lt;/code&gt;. Audio accepts a positional topic or nothing — it builds the podcast from your sources directly.&lt;/p&gt;

&lt;p&gt;One gotcha: &lt;code&gt;--wait&lt;/code&gt; can time out on slow generations (over 60 seconds). I always launch without waiting, get the task ID, then poll separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Launch generation without waiting&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm generate infographic &lt;span class="s2"&gt;"Dark-themed cover for article about AI research tools, ocean palette"&lt;/span&gt; &lt;span class="nt"&gt;--orientation&lt;/span&gt; landscape &lt;span class="nt"&gt;--style&lt;/span&gt; professional &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="c"&gt;# → {"task_id": "...", "status": "pending"}&lt;/span&gt;

&lt;span class="c"&gt;# Poll in a loop until done&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; notebooklm artifact poll &amp;lt;task_id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the status flips to &lt;code&gt;completed&lt;/code&gt;, a Google Cloud Storage URL is returned. I download that asset and route it to the right platform folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring It Into the Publishing Pipeline
&lt;/h2&gt;

&lt;p&gt;The full flow in Hermes Agent looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source material (markdown, PDFs, URLs)
        ↓
NotebookLM notebook created
        ↓
Sources added → NotebookLM indexes and summarizes
        ↓
Artifacts generated (infographic, mind-map, audio, slides)
        ↓
Hermes downloads, compresses, renames assets
        ↓
Platform-specific routing:
   Dev.to      → full article with embedded infographic
   Telegram    → voice message (audio compressed to 48kbps mono)
   LinkedIn    → carousel from slide deck PDF
   Medium      → essay with timeline / quote cards
   Bluesky     → dense teaser + link to full article
   Mastodon    → threaded summary with cover image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole sequence runs as a single Hermes skill. I trigger it with one line: &lt;code&gt;generate notebooklm article for &amp;lt;topic&amp;gt;&lt;/code&gt;. The agent creates the notebook, waits for artifacts, downloads them, compresses audio for Telegram's 20 MB limit, and writes platform drafts in parallel.&lt;/p&gt;

&lt;p&gt;No more clicking through Google UI. No more drag-and-drop. The AI generates; the pipeline publishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Working Integration Script
&lt;/h2&gt;

&lt;p&gt;Here is the core pattern I use inside the Hermes skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;notebooklm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NotebookLMClient&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NotebookLMClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Create notebook
&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_notebook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stoic Productivity Analysis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Add sources — mix of URLs and research queries
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/seneca-essay.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_research&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;modern applications of Stoic principles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Generate cover image (infographic)
&lt;/span&gt;&lt;span class="n"&gt;cover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_artifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;artifact_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infographic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dark-themed cover image for a technical article about AI-powered research tools. Ocean color palette.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 4. Poll for completion
&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll_artifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Artifact failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 5. Download
&lt;/span&gt;&lt;span class="n"&gt;cover_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cover_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assets/generated/cover.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 6. Generate audio podcast
&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_artifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifact_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;audio_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll_artifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 7. Download
&lt;/span&gt;&lt;span class="n"&gt;audio_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assets/generated/podcast.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 8. Compress for Telegram (20 MB limit)
&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffmpeg -i assets/generated/podcast.mp3 -b:a 48k -ac 1 assets/generated/podcast_tg.mp3&lt;/span&gt;&lt;span class="sh"&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 &lt;code&gt;NotebookLMClient&lt;/code&gt; reads the same &lt;code&gt;storage_state.json&lt;/code&gt; I built from Chrome cookies. No login step, no browser puppetry. Just API calls against a valid Google session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Fits in the Larger Stack
&lt;/h2&gt;

&lt;p&gt;NotebookLM is not where content starts — it is where research gets processed. My pipeline already had source material (markdown drafts, PDFs, web pages). NotebookLM adds three things other tools do not:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audio Overview&lt;/strong&gt; — two-voice podcast generated from your sources. I drop this straight into Telegram as a voice message. Compression takes a 30 MB raw file down to 7-10 MB. Voices stay clear at 48 kbps mono.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Infographics&lt;/strong&gt; — article covers and process diagrams generated from a text description. Before this I used Python Pillow locally. It worked, but every cover looked like the same template with slightly different text. NotebookLM generates unique styles per article. I just describe what I want.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mind Maps&lt;/strong&gt; — architecture and concept maps that I embed in Dev.to and Medium articles. The output includes node coordinates in JSON, so I can render it or let NotebookLM produce the visual directly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I still write the original text. I still decide the angle and the argument. But the repetitive work — cover design, audio production, diagram drawing — moved from my desk to the agent's queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke Along the Way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Playwright login is dead.&lt;/strong&gt; Every Chromium and Firefox instance I tried was detected by Google. "Browser not secure" at the login screen, every time. The cookie export method is the only approach that still works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong prompt syntax.&lt;/strong&gt; I spent an hour debugging &lt;code&gt;generate infographic --prompt "..."&lt;/code&gt; before realizing the flag does not exist. Positional arguments only. For mind-map, use &lt;code&gt;--instructions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio timing.&lt;/strong&gt; NotebookLM audio generation takes 5-10 minutes. If I use &lt;code&gt;--wait&lt;/code&gt;, the CLI sometimes times out after 60 seconds. I now always launch with &lt;code&gt;--json&lt;/code&gt; to get the task ID, then poll in a loop until &lt;code&gt;completed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image size limits.&lt;/strong&gt; Bluesky caps images at 2 MB. Raw NotebookLM PNGs are often 2.3-2.8 MB. I run them through &lt;code&gt;ffmpeg&lt;/code&gt; for JPEG compression at 800px width before uploading. Yes, ffmpeg — it handles image scaling too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results and What Changed
&lt;/h2&gt;

&lt;p&gt;Content production dropped from research plus design plus publishing to research plus about two minutes. The bottleneck is no longer tooling — it is deciding what to write. NotebookLM handles visuals and audio. Hermes handles routing and platform formatting. I handle ideas.&lt;/p&gt;

&lt;p&gt;The full pipeline code is open-source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/AzamatSafarov/hermes-notebooklm-bridge" rel="noopener noreferrer"&gt;https://github.com/AzamatSafarov/hermes-notebooklm-bridge&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It includes the cookie converter, the one-command pipeline, a Chrome bookmarklet for one-click cookie export, and a working example of Hermes Agent integration. If you are already using Hermes, you can slot it in with two CLI commands and a cookie file.&lt;/p&gt;

&lt;p&gt;This whole pipeline runs on the &lt;strong&gt;LLM Wiki&lt;/strong&gt; pattern by Andrej Karpathy — a persistent wiki that the LLM builds and maintains as you go, instead of RAG that re-discovers everything from scratch on every query.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f#file-llm-wiki-md" rel="noopener noreferrer"&gt;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f#file-llm-wiki-md&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Medium&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://medium.com/@akutagavasora777" rel="noopener noreferrer"&gt;https://medium.com/@akutagavasora777&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paragraph&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://paragraph.com/@azamatsafarov" rel="noopener noreferrer"&gt;https://paragraph.com/@azamatsafarov&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;X&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://x.com/Azamat__Safarov" rel="noopener noreferrer"&gt;https://x.com/Azamat__Safarov&lt;/a&gt;&lt;/p&gt;

</description>
      <category>notebooklm</category>
      <category>hermes</category>
      <category>automation</category>
      <category>publishing</category>
    </item>
    <item>
      <title>Building an Autoposting Pipeline with Hermes Agent: Why Waterfall Beats Parallel, and the Edge Cases Nobody Talks About</title>
      <dc:creator>Azamat Safarov</dc:creator>
      <pubDate>Mon, 25 May 2026 07:07:31 +0000</pubDate>
      <link>https://dev.to/azamat_safarov/building-an-autoposting-pipeline-with-hermes-agent-why-waterfall-beats-parallel-and-the-edge-1cjj</link>
      <guid>https://dev.to/azamat_safarov/building-an-autoposting-pipeline-with-hermes-agent-why-waterfall-beats-parallel-and-the-edge-1cjj</guid>
      <description>&lt;p&gt;&lt;em&gt;I write every day. Distributing to 8 platforms used to take 50 minutes. Now it takes 90 seconds. Here's what I learned building the pipeline, why waterfall beats parallel, and every API trap that almost made me quit.&lt;/em&gt;&lt;/p&gt;

&lt;h2&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.amazonaws.com%2Fuploads%2Farticles%2Fb6ns16o3p1c9frt2skld.png" alt=" " width="800" height="854"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I write every day. For two years, every article ended the same way: open eight tabs, copy the markdown, strip formatting for Telegram, compress images for Mastodon, rewrite the hook for Bluesky's 300-character limit, paste URLs into each platform, log what went where. Fifty minutes of mechanical work after the creative part was done.&lt;/p&gt;

&lt;p&gt;The real cost isn't time. It's context switching. After four hours of writing, switching to "platform adaptation mode" feels like starting a second job. You think "I'll distribute tomorrow" and tomorrow you don't remember what the article was about.&lt;/p&gt;

&lt;p&gt;I tried Buffer. Tried Zapier. Both fail on the same two fronts: they don't handle markdown-to-plaintext adaptation (Telegram and VK show &lt;code&gt;**bold**&lt;/code&gt; literally, not bold), and they don't support Bluesky AT Protocol or Paragraph GraphQL. For niche platforms, custom publishers are the only path.&lt;/p&gt;

&lt;p&gt;So I built one. Eight platforms, four pipeline stages, one command.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Why Waterfall, Not Parallel
&lt;/h2&gt;

&lt;p&gt;First attempt was naive: blast all platforms simultaneously. Failed immediately.&lt;/p&gt;

&lt;p&gt;The issue is dependency chains. Bluesky teasers need the WordPress URL to build link cards — you can't post a teaser before the canonical article exists. Dev.to and Paragraph have rate limits that, when hit in parallel, cascade into 429 storms across all publishers. One platform choking kills the entire run.&lt;/p&gt;

&lt;p&gt;Solution: waterfall with selective concurrency. Four stages, each producing artifacts the next stage consumes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────┐
│  PIPELINE: WATERFALL WITH SELECTIVE CONCURRENCY                       │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Stage 1: Content Adaptation (sequential)                           │
│    → Parse markdown source                                          │
│    → Generate platform-specific variants via Jinja2 templates       │
│    → Compress images per platform limits (FFmpeg)                    │
│                                                                     │
│  Stage 2: Primary Hub (sequential, blocking)                        │
│    → WordPress: publish full article → get canonical URL            │
│    → Dev.to: publish markdown variant → get dev.to URL              │
│                                                                     │
│  Stage 3: Social Teasers (parallel, 4 threads)                     │
│    → Bluesky: 300 chars + compressed image + link                   │
│    → Mastodon: 500 chars + media upload + link                      │
│    → Tumblr: photo post + caption + link                            │
│    → Paragraph: markdown + external image URLs + link               │
│                                                                     │
│  Stage 4: Archive (sequential)                                      │
│    → Git commit with all published URLs                             │
│    → Update LLM-Wiki index                                          │
│    → Write execution log for debugging                              │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Full architecture: LLM-Wiki vault → adaptation → 20+ platforms. Hermes Agent orchestrates each stage.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why waterfall?&lt;/strong&gt; Each stage produces artifacts the next stage consumes. WordPress URL becomes the canonical reference for all social platforms. Without it, you're posting orphaned content that disappears in 48 hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1: Content Adaptation
&lt;/h2&gt;

&lt;p&gt;This is where most pipelines die silently. You can't post the same markdown to Dev.to, Telegram, and Bluesky. Dev.to renders &lt;code&gt;##&lt;/code&gt; headers natively. Telegram shows raw &lt;code&gt;#&lt;/code&gt; symbols. Bluesky strips all markdown and shows plain text. Each platform needs its own variant.&lt;/p&gt;

&lt;p&gt;I built an adaptation layer with Jinja2 templates — one per platform. The source is always the same markdown file in the LLM-Wiki vault. The adapter reads it, applies platform rules, and generates the variant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/adaptation.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jinja2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileSystemLoader&lt;/span&gt;

&lt;span class="n"&gt;TEMPLATES_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;templates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FileSystemLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEMPLATES_DIR&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adapt_for_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Generate platform-specific variant from canonical markdown.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.j2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Platform-specific preprocessing
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Strip all markdown — these platforms don't render it
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\*\*(.*?)\*\*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# bold
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\*(.*?)\*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;# italic
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__(.*?)__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# underline
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;~~(.*?)~~&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# strikethrough
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\[(.*?)\]\((.*?)\)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# links → bare URL
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;!\[.*?\]\(.*?\)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# remove images
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#{1,6}\s+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# remove headers
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;has_images&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bluesky&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# 300 graphemes hard limit — distill to hook + detail + URL
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#{1,6}\s+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\*\*(.*?)\*\*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\n+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Leave room for URL (~30 chars)
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;270&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Markdown-native but SVG doesn't render
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source_md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.svg)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.png)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# rough SVG→PNG swap
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;extract_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_md&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wordpress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Full HTML conversion for WordPress REST API
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;markdown_to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_md&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mastodon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# 500 chars, no markdown
&lt;/span&gt;        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\*\*(.*?)\*\*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\n+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;470&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paragraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Markdown accepted, but images must be external URLs
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source_md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;images_are_external&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source_md&lt;/span&gt;  &lt;span class="c1"&gt;# fallback
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extract tags from YAML frontmatter, limit to 4 for Dev.to.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^tags:\s*(.+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MULTILINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Dev.to hard limit
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;automation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Platform rules (hard-won through failure):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Markdown&lt;/th&gt;
&lt;th&gt;Images&lt;/th&gt;
&lt;th&gt;Length Limit&lt;/th&gt;
&lt;th&gt;Special Rules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Telegram / VK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strip all&lt;/td&gt;
&lt;td&gt;External URL previews only&lt;/td&gt;
&lt;td&gt;~4000 chars&lt;/td&gt;
&lt;td&gt;Bare URLs for previews, no markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bluesky&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strip all&lt;/td&gt;
&lt;td&gt;Blob upload (2MB max)&lt;/td&gt;
&lt;td&gt;300 graphemes&lt;/td&gt;
&lt;td&gt;Must count graphemes, not bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mastodon&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strip all&lt;/td&gt;
&lt;td&gt;Two-step media upload&lt;/td&gt;
&lt;td&gt;500 chars&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /api/v1/media&lt;/code&gt; → &lt;code&gt;id&lt;/code&gt; → &lt;code&gt;POST /api/v1/statuses&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dev.to&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Must be PNG (SVG breaks)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;4 tags max, draft mode available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WordPress&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;External URLs only (free plan)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;posts&lt;/code&gt; scope only, no &lt;code&gt;media&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Paragraph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;External URLs only&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;GitHub raw URLs hotlink-blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The adaptation stage takes ~30 seconds — reads source, applies all rules, writes 8 variants. Each variant is git-versioned.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2: Primary Hub — WordPress &amp;amp; Dev.to
&lt;/h2&gt;

&lt;p&gt;Two platforms publish first. Both produce URLs that downstream platforms need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WordPress&lt;/strong&gt; is the SEO anchor. Every social teaser links back to it as canonical source. Social platforms are not indexed by Google; WordPress is. Without it, you're posting orphaned content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# api/publishers/wordpress.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;ACCESS_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WORDPRESS_ACCESS_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;BLOG_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WORDPRESS_BLOG_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;featured_image_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://public-api.wordpress.com/rest/v1.2/sites/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BLOG_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/posts/new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACCESS_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;featured_image_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;featured_image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;featured_image_url&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ID&lt;/span&gt;&lt;span class="sh"&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;&lt;strong&gt;Critical limitation:&lt;/strong&gt; Free WordPress.com plans only grant &lt;code&gt;posts&lt;/code&gt; OAuth scope. &lt;code&gt;media&lt;/code&gt; scope requires paid plan. Cannot upload images via API on free tier. Workaround: host images externally and reference by URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dev.to&lt;/strong&gt; is the technical hub. Markdown-native, code blocks work, frontmatter tags auto-categorize. The Dev.to URL becomes the "also published on" link.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# api/publishers/devto.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DEVTO_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_article&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/api/articles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body_markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Dev.to hard limit: 4 tags max
&lt;/span&gt;        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Invisible limits discovered:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4 tags maximum.&lt;/strong&gt; Sending 5 returns 422 &lt;code&gt;"Tag list exceed the maximum of 4 tags"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG images don't render.&lt;/strong&gt; Dev.to's CDN doesn't process SVG. Convert to PNG via FFmpeg before upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft mode:&lt;/strong&gt; &lt;code&gt;published: false&lt;/code&gt; creates a draft visible in your dashboard. You review, then hit "Publish" manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both must complete before Stage 3 starts. If WordPress fails, the pipeline halts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 3: Social Teasers — Parallel
&lt;/h2&gt;

&lt;p&gt;Once WordPress and Dev.to return URLs, four social platforms fire in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bluesky: AT Protocol Is Not REST&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bluesky doesn't use REST. It uses AT Protocol — binary blob uploads, record creation via JSON-RPC, 2MB blob size limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# api/publishers/bluesky.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="n"&gt;HANDLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BLUESKY_HANDLE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;APP_PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BLUESKY_APP_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://bsky.social/xrpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/com.atproto.server.createSession&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HANDLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;APP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accessJwt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accessJwt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accessJwt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;did&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;did&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;mime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/jpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/jpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gif&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/gif&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/com.atproto.repo.uploadBlob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accessJwt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mime&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;  &lt;span class="c1"&gt;# 1.5MB uploads need time
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blob&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;

    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.bsky.feed.post&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createdAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&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="n"&gt;image_path&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getsize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2_000_000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-compressed.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffmpeg -y -i &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -q:v 2 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;image_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;compressed&lt;/span&gt;

        &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;upload_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embed&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.bsky.embed.images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/com.atproto.repo.createRecord&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accessJwt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;did&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;collection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.bsky.feed.post&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;post_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://bsky.app/profile/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HANDLE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/post/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Three traps in this code:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;uploadBlob&lt;/code&gt; expects raw bytes with &lt;code&gt;Content-Type&lt;/code&gt; header — not multipart &lt;code&gt;files={...}&lt;/code&gt;. &lt;code&gt;requests.post(..., data=bytes, headers={"Content-Type": "image/png"})&lt;/code&gt; — not &lt;code&gt;files={"image": open(...)}&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Default 30-second timeout is too short for 1.5MB uploads. Increased to 120 seconds after three consecutive failures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;300-grapheme limit is hard. &lt;code&gt;len(text)&lt;/code&gt; counts codepoints; Japanese emoji counts as multiple grapemes. Must use &lt;code&gt;regex.findall(r'\X', text)&lt;/code&gt; for accurate counting.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Mastodon: Two-Step Media&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mastodon requires separate media upload before status creation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# api/publishers/mastodon.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MASTODON_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ACCESS_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MASTODON_ACCESS_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload_media&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/v1/media&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACCESS_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;media_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/v1/statuses&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACCESS_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;media_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media_ids[]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;media_ids&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&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;Single-call status creation with both text and file does NOT work. Must be two requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tumblr&lt;/strong&gt; and &lt;strong&gt;Paragraph&lt;/strong&gt; follow similar patterns — media upload first, then post creation with media IDs or external URLs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4: Archive &amp;amp; The Orchestrator
&lt;/h2&gt;

&lt;p&gt;Hermes Agent schedules tasks but never autopublishes without explicit approval.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/orchestrator.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&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="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;api.publishers.bluesky&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;publish_bluesky&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;api.publishers.wordpress&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;publish_post&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;publish_wordpress&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;api.publishers.devto&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;publish_article&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;publish_devto&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;api.publishers.mastodon&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;post_status&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;publish_mastodon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upload_media&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;upload_mastodon_media&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;api.publishers.paragraph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;publish_post&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;publish_paragraph&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scripts.adaptation&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;adapt_for_platform&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;full_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Stage 1: Adaptation
&lt;/span&gt;    &lt;span class="n"&gt;variants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wordpress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;adapt_for_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wordpress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;adapt_for_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bluesky&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;adapt_for_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bluesky&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mastodon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;adapt_for_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mastodon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tumblr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;adapt_for_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tumblr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paragraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;adapt_for_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paragraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DRY RUN] Platform variants generated:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Stage 2: Primary Hub (sequential, blocking)
&lt;/span&gt;    &lt;span class="n"&gt;wp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;publish_wordpress&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Article Title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wordpress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Productivity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;automation, python, publishing, hermes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;canonical_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;publish_devto&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Article Title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body_markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Draft for review
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;automation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publishing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hermes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Stage 3: Social Teasers (parallel, 4 threads)
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_bluesky_trailer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bluesky&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; → &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;canonical_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;publish_bluesky&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_mastodon_trailer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;media&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;upload_mastodon_media&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mastodon&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; → &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;publish_mastodon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;media&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;media&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_tumblr_post&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# Tumblr API: photo post with caption
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;publish_tumblr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tumblr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_paragraph_article&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;publish_paragraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Article Title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paragraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bluesky&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publish_bluesky_trailer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mastodon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publish_mastodon_trailer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tumblr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publish_tumblr_post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paragraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publish_paragraph_article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;social_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

    &lt;span class="c1"&gt;# Stage 4: Archive
&lt;/span&gt;    &lt;span class="nf"&gt;log_publish_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;social_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;git_commit_with_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;social_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;update_llm_wiki_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;social_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Human-in-the-loop design:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User says "publish this article"&lt;/li&gt;
&lt;li&gt;Agent generates all variants, shows previews&lt;/li&gt;
&lt;li&gt;User reviews Dev.to draft (most complex variant)&lt;/li&gt;
&lt;li&gt;User says "confirmed" — only then API calls execute&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This prevents the "oh no I just live-posted a draft" panic. Every publisher has &lt;code&gt;dry_run=True&lt;/code&gt; default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure handling:&lt;/strong&gt; If Bluesky fails, the pipeline logs the error, continues with remaining platforms, reports partial success:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bluesky: FAILED (blob &amp;gt; 2MB)
WordPress: SUCCESS — https://azamatsafarov.wordpress.com/...
Dev.to: SUCCESS (draft) — https://dev.to/...
Mastodon: SUCCESS — https://mastodon.social/@...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Edge Cases: The Real Curriculum
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Edge Case&lt;/th&gt;
&lt;th&gt;How I Discovered It&lt;/th&gt;
&lt;th&gt;The Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bluesky blob &amp;gt; 2MB&lt;/td&gt;
&lt;td&gt;Three consecutive failures Thursday evening, all 400 with no error body&lt;/td&gt;
&lt;td&gt;FFmpeg compression: &lt;code&gt;ffmpeg -y -i input.png -q:v 2 output.jpg&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress free plan blocks media upload&lt;/td&gt;
&lt;td&gt;403 on every image upload, docs say "check token scope"&lt;/td&gt;
&lt;td&gt;Host images externally, reference by URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev.to SVG broken&lt;/td&gt;
&lt;td&gt;Pipeline diagram showed empty box for a week&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ffmpeg -y -i diagram.svg diagram.png&lt;/code&gt; in adaptation stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev.to 5th tag rejected&lt;/td&gt;
&lt;td&gt;Got 422, read error, realized the limit&lt;/td&gt;
&lt;td&gt;Hard truncate to 4 tags. No negotiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mastodon two-step media&lt;/td&gt;
&lt;td&gt;Posted text without image three times&lt;/td&gt;
&lt;td&gt;Two separate requests: &lt;code&gt;POST /api/v1/media&lt;/code&gt; → &lt;code&gt;id&lt;/code&gt; → &lt;code&gt;POST /api/v1/statuses&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bluesky 300 graphemes&lt;/td&gt;
&lt;td&gt;Posted 305-char teaser, got &lt;code&gt;"record too large"&lt;/code&gt; with no details&lt;/td&gt;
&lt;td&gt;Grapheme counting via &lt;code&gt;regex.findall(r'\X', text)&lt;/code&gt; before posting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paragraph hotlink blocking&lt;/td&gt;
&lt;td&gt;GitHub raw URLs showed as broken images&lt;/td&gt;
&lt;td&gt;VK CDN URLs from &lt;code&gt;photos.getById&lt;/code&gt; after VK upload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn token expires&lt;/td&gt;
&lt;td&gt;Posts failed with 401 after 60 days&lt;/td&gt;
&lt;td&gt;Manual refresh. Not worth automating for monthly posting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown divergence&lt;/td&gt;
&lt;td&gt;Telegram showed &lt;code&gt;**bold**&lt;/code&gt; literally, VK showed raw brackets&lt;/td&gt;
&lt;td&gt;Strip all markdown for Telegram/VK in adaptation stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NotebookLM cookies expire&lt;/td&gt;
&lt;td&gt;Cron job ran at 11:00, failed silently at auth&lt;/td&gt;
&lt;td&gt;Two-step cron: 10:30 reminder → user pastes cookies → 11:00 publish&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Results: What Actually Changed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before Pipeline&lt;/th&gt;
&lt;th&gt;After Pipeline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time per article&lt;/td&gt;
&lt;td&gt;50 minutes (8 platforms × ~6 min each)&lt;/td&gt;
&lt;td&gt;90 seconds (automation) + 5 min review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platforms covered&lt;/td&gt;
&lt;td&gt;3–4, depending on energy&lt;/td&gt;
&lt;td&gt;8 automated + drafts for 10 more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed uploads&lt;/td&gt;
&lt;td&gt;~30% (forgot image, wrong format, expired token)&lt;/td&gt;
&lt;td&gt;~5% (token expiration only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO indexing&lt;/td&gt;
&lt;td&gt;None — social content ephemeral&lt;/td&gt;
&lt;td&gt;WordPress as canonical, indexed by Google&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context switching&lt;/td&gt;
&lt;td&gt;8 tabs, 8 different UIs, 8 copy-paste operations&lt;/td&gt;
&lt;td&gt;One command, one preview, one approval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Article versioning&lt;/td&gt;
&lt;td&gt;None — distributed copies diverged&lt;/td&gt;
&lt;td&gt;Git tracks every variant, platform, URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest change isn't speed. It's consistency. Before the pipeline, I distributed to 3–4 platforms depending on how tired I was. The pipeline makes 8 platforms the default. Energy-independent.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>architecture</category>
      <category>hermes</category>
    </item>
  </channel>
</rss>
