<?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: Ronny Cruz</title>
    <description>The latest articles on DEV Community by Ronny Cruz (@candornetwork).</description>
    <link>https://dev.to/candornetwork</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%2F4033883%2Fd495ac32-d2da-4899-98b8-f57d42092255.png</url>
      <title>DEV Community: Ronny Cruz</title>
      <link>https://dev.to/candornetwork</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/candornetwork"/>
    <language>en</language>
    <item>
      <title>Explain X Like I'm Five</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Thu, 06 Aug 2026 23:12:34 +0000</pubDate>
      <link>https://dev.to/candornetwork/explain-x-like-im-five-4e6i</link>
      <guid>https://dev.to/candornetwork/explain-x-like-im-five-4e6i</guid>
      <description>&lt;p&gt;How to learn to use node.js?&lt;/p&gt;

</description>
      <category>explainlikeimfive</category>
    </item>
    <item>
      <title>Candor Trust and Safety</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:31:47 +0000</pubDate>
      <link>https://dev.to/candornetwork/candor-trust-and-safety-2jfl</link>
      <guid>https://dev.to/candornetwork/candor-trust-and-safety-2jfl</guid>
      <description>&lt;p&gt;Most "trust and safety" pages are a paragraph of promises. We built ours differently.&lt;/p&gt;

&lt;p&gt;This week we launched Candor Trust &amp;amp; Safety — the public home for every safety system behind Candor: The Open Feed Network. Not a marketing page. An evidence page.&lt;/p&gt;

&lt;p&gt;A few things that make it different:&lt;/p&gt;

&lt;p&gt;🔑 Our cryptographic keys are published, not just claimed. Every adult-verification we issue is signed with a key you can look up yourself — no account, no API, no trusting our word for it.&lt;/p&gt;

&lt;p&gt;🧾 Every consent record is independently checkable. Paste a receipt into our verifier and your own browser — not our servers — confirms it's real. If we ever get it wrong, anyone can catch it. That's the point.&lt;/p&gt;

&lt;p&gt;🧒 Your Voice, our youth platform, now requires ID-verified adult consent for every account — enforced in the code, not just in policy. No verified adult, no account. Full stop.&lt;/p&gt;

&lt;p&gt;🛡️ We red-team our own systems and publish what we find. This week our internal adversarial testing tool caught a real gap in our own consent flow — we fixed it, then proved the fix twice: once against the exact attack that found it, once confirming real users were never affected.&lt;/p&gt;

&lt;p&gt;None of this is because regulation forced our hand (though it's coming fast — the UK, the EU, half of US states). It's because "trust us" was never going to be good enough, and we'd rather build something you don't have to.&lt;/p&gt;

&lt;p&gt;Take a look: candortrustandsafety.com&lt;/p&gt;

&lt;h1&gt;
  
  
  TrustAndSafety #OnlineSafety #AgeVerification #BuildInPublic #ChildSafety
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>architecture</category>
      <category>startup</category>
    </item>
    <item>
      <title>The Compatibility Claim I Hadn't Earned</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Fri, 24 Jul 2026 19:20:54 +0000</pubDate>
      <link>https://dev.to/candornetwork/the-compatibility-claim-i-hadnt-earned-19i0</link>
      <guid>https://dev.to/candornetwork/the-compatibility-claim-i-hadnt-earned-19i0</guid>
      <description>&lt;p&gt;By Ronny Cruz , founder of Open Feed Network (Candor Network). Solo founder, one production platform, and — as of this week — one very small Fediverse instance.&lt;/p&gt;

&lt;p&gt;The line in my own README&lt;/p&gt;

&lt;p&gt;Sentinel Signup is a registration-screening service for Fediverse instances. It reads your pending signup queue, scores each applicant, and approves or rejects through the admin API. I launched it during the July spam wave, when instance admins were drowning in fake approval requests.&lt;/p&gt;

&lt;p&gt;Here's the setup instruction I shipped with it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Mastodon: Preferences → Development → New application, scopes &lt;code&gt;admin:read admin:write&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that again. The product says &lt;em&gt;Fediverse&lt;/em&gt;. The documentation says &lt;em&gt;Mastodon&lt;/em&gt;. Those are not the same word, and the gap between them was a claim I had no evidence for.&lt;/p&gt;

&lt;p&gt;The Fediverse is not one piece of software. It's Mastodon and GoToSocial and Akkoma and Pleroma and Misskey and Sharkey and Iceshrimp and a dozen others, most of which implement "the Mastodon client API" to varying and undocumented degrees. My sidecar spoke four endpoints of that API. I had verified them against exactly zero servers, because I had never run an instance in my life.&lt;/p&gt;

&lt;p&gt;I was selling anti-abuse tooling to a community I wasn't a member of. That's not a moral failing, but it is an epistemic one: everything I knew about how those servers behave, I knew from reading specifications. Specifications describe intent. Deployments have opinions.&lt;/p&gt;

&lt;p&gt;So I built the testbed I should have had first&lt;/p&gt;

&lt;p&gt;The constraint was that it had to cost nothing. Mastodon itself is a heavy tenant — Ruby, Sidekiq, Redis, its own Postgres — and the box I'd be putting it on already runs five production services. GoToSocial is a single Go binary with SQLite that idles in a couple hundred megabytes, and it implements the Mastodon client API. That's the whole reason it was the right choice: it's &lt;em&gt;not&lt;/em&gt; Mastodon, which makes it a real test rather than a rehearsal.&lt;/p&gt;

&lt;p&gt;Two things went wrong before it ever started, and both are worth writing down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CPU refused.&lt;/strong&gt; GoToSocial's standard build runs its bundled ffmpeg and SQLite through wazero, a WebAssembly runtime, and wazero's compiler requires x86-64-v2 instructions. My VPS reports itself as a generic QEMU virtual CPU — no SSE4.2, no SSSE3, no POPCNT — because that's what many budget and older virtualized hosts advertise. GoToSocial doesn't degrade to interpreter mode; it panics on purpose, which I respect. The fix is an official &lt;code&gt;nowasm&lt;/code&gt; build that uses the system's native ffmpeg instead. If you're self-hosting anything on inexpensive virtualization, check &lt;code&gt;/proc/cpuinfo&lt;/code&gt; before you assume modern instruction sets. Mine had been quietly ancient for months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The proxy lied about who was knocking.&lt;/strong&gt; The instance sits behind Cloudflare, and my nginx config forwarded the client address the conventional way. Behind a CDN, that address is the CDN's — every signup on Earth would have arrived from the same handful of edge IPs.&lt;/p&gt;

&lt;p&gt;That one is worth dwelling on, because of how it fails. Nothing errors. The screening service still returns verdicts. The logs still look healthy. It just silently becomes blind in the exact dimension it's most valuable in: IP reputation, subnet velocity, burst detection — all the signals that catch a bot farm — collapse into "one address, always." A safety system that reports success while measuring nothing is worse than one that's obviously broken, which is a lesson I keep relearning in new costumes. The fix was one nginx directive reading the CDN's real-client header. The habit is: after any infrastructure change, verify that your signals still carry information, not just that your requests still return 200.&lt;/p&gt;

&lt;p&gt;What verification actually looked like&lt;/p&gt;

&lt;p&gt;With the node running and reachable, I checked the four calls my sidecar makes, one at a time, against a server I'd never tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v2/admin/accounts&lt;/code&gt; — 200&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v1/admin/accounts&lt;/code&gt; (the fallback path) — 200&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v2/instance&lt;/code&gt; — 200&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/admin/accounts/{id}/reject&lt;/code&gt; — 200&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the question that actually determined whether the product is useful on GoToSocial at all: &lt;strong&gt;does the admin API expose the signup IP?&lt;/strong&gt; Mastodon does. If GoToSocial didn't, then on GTS instances Sentinel would be running on username and application text alone — still something, but a fraction of the defense, and I'd have to say so out loud in the documentation.&lt;/p&gt;

&lt;p&gt;It does. The &lt;code&gt;ip&lt;/code&gt; field came back populated. Velocity, subnet, and reputation all function. The compatibility claim survives without a caveat, and — this is the part that matters — it survives because I ran it, not because I assumed it.&lt;/p&gt;

&lt;p&gt;I also found two differences worth documenting. GoToSocial places a signup into the pending queue immediately, without waiting for email confirmation, which Mastodon doesn't. And its admin CLI has no &lt;code&gt;delete&lt;/code&gt; for accounts at all — only &lt;code&gt;disable&lt;/code&gt;, plus rejection through the API. Neither breaks anything. Both would have been surprises in front of a customer.&lt;/p&gt;

&lt;p&gt;The first real signup&lt;/p&gt;

&lt;p&gt;Then I registered a throwaway account on my own instance, in an incognito window, like a stranger would, and wrote a genuine sentence in the reason field.&lt;/p&gt;

&lt;p&gt;Thirty seconds later the sidecar picked it out of the queue, sent it to the live screening service, and logged:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WOULD APPROVE: @[redacted] verdict=pass confidence=1 — Clean IP history and natural application language.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Dry run, so it did nothing, which is exactly what dry run is for. But the whole chain had run for the first time in its life: pending signup → admin API → screening engine → verdict → action decision. Every piece I'd built separately, working together, on a server type I'd never touched.&lt;/p&gt;

&lt;p&gt;Rejecting the test account afterward returned 200 and cleared the queue to zero, which incidentally verified the last endpoint.&lt;/p&gt;

&lt;p&gt;I left it in dry run, on purpose&lt;/p&gt;

&lt;p&gt;The obvious next move is flipping it live so my own instance is auto-screened. I didn't.&lt;/p&gt;

&lt;p&gt;My instance isn't a community — it's one account and a testbed. Turning on auto-approval means strangers get accounts, which means I'm hosting federated content, which means I've added a moderation surface to a machine already running five production services. That's a real decision with real obligations, not a config flag, and I'm not making it as a side effect of testing something.&lt;/p&gt;

&lt;p&gt;So the sidecar runs continuously in dry run: every future signup gets screened and logged, giving me the observations, while approval stays a human decision. The testbed value is fully there. The obligation isn't taken on accidentally. I can flip it whenever I mean to.&lt;/p&gt;

&lt;p&gt;Honest limitations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One instance, one signup, my own IP.&lt;/strong&gt; This proves the plumbing connects. It is not a load test, not an adversarial test, and not evidence about accuracy. Nobody has attacked this instance, because nobody knows it exists yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One version.&lt;/strong&gt; GoToSocial 0.22.1. API coverage moves between releases; a claim verified today is a claim about today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Fediverse-compatible" is still an overclaim, and I'm retiring it.&lt;/strong&gt; What I can now say is: Mastodon and GoToSocial, verified. Akkoma, Pleroma, Misskey, Sharkey, Iceshrimp — untested. If you run one of those and want to find out with me, I'll do the work with you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;nowasm&lt;/code&gt; build is officially unsupported&lt;/strong&gt; for media processing. Fine for a testbed. Worth knowing before you put it under a real community.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I use AI tooling heavily&lt;/strong&gt; — Claude, mostly — to build and to write, including this post. The screening engine itself is deliberately heuristic and rule-based with no model in the loop, because it has to run at effectively zero marginal cost for instances that can't pay for spam protection. Where the machines helped, they helped; where the evidence comes from, it's my own server's logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point of being customer zero&lt;/p&gt;

&lt;p&gt;"Built for ourselves, available to you" has been the pitch since the safety modules were extracted from my own platform. This week it got more literal: my platform's registration is screened by this product, and now a second server type is too, on infrastructure I run.&lt;/p&gt;

&lt;p&gt;The reason that matters isn't marketing. It's that every gap I just described — the CPU, the blinded IPs, the queue behavior, the missing CLI verb — would otherwise have been found by an admin under attack, at the worst possible moment, in a system they'd trusted on my word. Finding them cost me an evening and zero dollars. Finding them the other way costs someone their instance.&lt;/p&gt;

&lt;p&gt;If you run a Fediverse instance of any flavor and your signup queue is full of garbage, Sentinel Signup is free during beta: signup.candortheopenfeednetwork.com. If you run something I haven't verified, tell me and let's test it properly — &lt;a href="mailto:tips@candortheopenfeednetwork.com"&gt;tips@candortheopenfeednetwork.com&lt;/a&gt;. I answer everything myself.&lt;br&gt;
(&lt;a href="https://candortheopenfeednetwork.com/sentinel-signup" rel="noopener noreferrer"&gt;https://candortheopenfeednetwork.com/sentinel-signup&lt;/a&gt;)&lt;br&gt;
(&lt;a href="https://app.candortheopenfeednetwork.com" rel="noopener noreferrer"&gt;https://app.candortheopenfeednetwork.com&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>architecture</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>El Formulario de Registro Que No Tiene Nada Que Filtrar</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Thu, 23 Jul 2026 20:32:36 +0000</pubDate>
      <link>https://dev.to/candornetwork/el-formulario-de-registro-que-no-tiene-nada-que-filtrar-1i1b</link>
      <guid>https://dev.to/candornetwork/el-formulario-de-registro-que-no-tiene-nada-que-filtrar-1i1b</guid>
      <description>&lt;p&gt;Por Ronny Cruz, fundador de Open Feed Network (Candor Network). Fundador solo, una plataforma en producción, y un flujo de registro que deliberadamente no sabe casi nada de ti.&lt;/p&gt;

&lt;p&gt;El problema que me creé yo mismo&lt;/p&gt;

&lt;p&gt;El registro de Candor no recoge correo electrónico. Ni teléfono. Ni cuenta de CAPTCHA, ni identidad de OAuth, nada que verificar porque no hay nada contra qué verificar. Instalas la app, eliges un nombre de usuario, pones un PIN, y el navegador genera un par de llaves ECDSA P-256 en tu dispositivo. El servidor recibe exactamente dos cosas: el nombre de usuario y la llave pública. Las doce palabras semilla y el archivo de recuperación se quedan contigo. Ese es el registro completo.&lt;/p&gt;

&lt;p&gt;Lo construí así a propósito. Registro sin datos personales significa que no hay base de datos de correos que filtrar, ni lista de teléfonos que citar judicialmente, ni grafo de identidad que vender. Para una plataforma cuya tesis completa es que tu voz no debería depender de confiar en mí, es el diseño correcto.&lt;/p&gt;

&lt;p&gt;También es, desde la perspectiva de la defensa contra spam, una pesadilla que me entregué a mí mismo. Cada señal en la que la industria se apoya para filtrar registros —dominios de correo desechable, reputación de correo, verificación de ida y vuelta— asume que existe un correo. El mío no. Y el flujo es rápido por diseño, lo cual significa que un script también puede serlo. El viernes pasado, una revisión de código con IA lo dijo sin rodeos: nada impedía que un bot hiciera un bucle sobre el endpoint de registro y creara cincuenta mil cuentas, inundando el feed con identidades que no cuestan nada crear.&lt;/p&gt;

&lt;p&gt;La revisión tenía razón. Nada lo impedía.&lt;/p&gt;

&lt;p&gt;La ironía era que yo vendo la solución&lt;/p&gt;

&lt;p&gt;La semana anterior había lanzado Sentinel Signup — un servicio de filtrado de registros para instancias del Fediverso golpeadas por exactamente ese tipo de ola. Seis capas: reputación de IP y de dominio, velocidad por subred, patrones de tiempo, análisis de contenido, todo.&lt;/p&gt;

&lt;p&gt;Mi propia plataforma no lo estaba usando. Los hijos del zapatero, descalzos como siempre.&lt;/p&gt;

&lt;p&gt;Peor: no podía usarlo. La API de filtrado exigía un correo en cada petición, porque todos los clientes que había imaginado tenían uno. Mi propio flujo de registro —el más respetuoso con la privacidad que conozco— estaba estructuralmente excluido de mi propio producto de seguridad. Y el motor iba más lejos que solo exigirlo: un correo ausente sumaba una penalización de +30 por "correo inválido". Pasa a mis propios usuarios honestos por mi propio filtro, y todos empiezan pareciendo sospechosos. Una función de privacidad leída como señal de fraude.&lt;/p&gt;

&lt;p&gt;Vale la pena detenerse ahí, porque no es una peculiaridad de mi código. Es lo que pasa en toda esta industria cuando los sistemas anti-abuso asumen señales de identidad que las plataformas respetuosas con la privacidad deliberadamente no recogen. La postura por defecto de la defensa contra spam es que saber menos de un usuario lo hace más sospechoso. Si queremos que existan plataformas que preserven la privacidad, las herramientas de seguridad tienen que dejar de castigarlas por ello.&lt;/p&gt;

&lt;p&gt;La solución: ausencia no es malformación&lt;/p&gt;

&lt;p&gt;El cambio fue pequeño y filosóficamente importante. El motor ahora distingue dos cosas que antes confundía:&lt;/p&gt;

&lt;p&gt;Correo presente pero mal formado — se sigue penalizando. Alguien mandó basura en un campo de correo; eso es una señal real.&lt;br&gt;
Correo no proporcionado en absoluto — se registra como un hecho neutral, peso cero. Una plataforma sin datos personales no tiene nada que enviar, y ser honesta sobre eso no debería costarle nada a sus usuarios.&lt;br&gt;
Cada verificación que se activa cuando el correo sí está presente —listas de dominios desechables, patrones de correos temporales— se activa exactamente igual que antes. La batería de pruebas comprueba ambas direcciones: ninguna penalización por ausencia, ninguna suavización ante la basura.&lt;/p&gt;

&lt;p&gt;Con eso, mi registro por fin pudo ser filtrado por mi propio producto, usando solo lo que realmente existe: nombre de usuario, IP real del cliente (desde la cabecera reenviada por el CDN — el servicio nunca ve conexiones directas), agente de usuario y tiempo. Sin correo, sin marcador de posición sintético contaminando el análisis. Lo que es real se analiza; lo que no existe no se inventa.&lt;/p&gt;

&lt;p&gt;La pregunta de postura que nadie hace hasta que hay una caída&lt;/p&gt;

&lt;p&gt;Conectar el filtro al manejador de registro obligó a una decisión que la mayoría de las integraciones se saltan: ¿qué pasa cuando el filtrado mismo falla?&lt;/p&gt;

&lt;p&gt;Para mis clientes administradores de Mastodon, la respuesta es falla-a-cola — un error de filtrado envía al solicitante a la cola de aprobación humana, donde decide un moderador. Nunca admitido en silencio, nunca rechazado en silencio.&lt;/p&gt;

&lt;p&gt;Mi flujo de registro no tiene cola. No hay moderador entre una persona y su cuenta, por diseño. Así que falla-a-cola no significa nada aquí, y las dos opciones de manual están mal: fallar-cerrado significa que un tropiezo de dos segundos deja fuera a personas reales; fallar-abierto en silencio significa que los bots pasan y nadie se entera.&lt;/p&gt;

&lt;p&gt;La postura que desplegué: bloquear solo ante un veredicto duro de bloqueo; ante una marca o cualquier error, permitir — pero en voz alta. Cada registro marcado y cada fallo de filtrado queda en los logs con su razón. Una ráfaga que se cuele durante una caída es visible y atribuible en minutos. Una persona real bloqueada por un fallo de dependencia sería invisible para siempre — simplemente se iría. Entre esos dos costos, la decisión no está reñida.&lt;/p&gt;

&lt;p&gt;Qué le hizo la barrera al escenario del bot&lt;/p&gt;

&lt;p&gt;Después de desplegar, corrí el ataque contra mi propio servidor de producción. Primero, un registro legítimo — pasó limpio, token emitido, indistinguible de antes. Luego la versión en miniatura del escenario de las cincuenta mil cuentas: registros a toda velocidad desde una sola IP, par de llaves nuevo cada vez.&lt;/p&gt;

&lt;p&gt;La escalera de escalado, directo de los logs:&lt;/p&gt;

&lt;p&gt;Registros 1–2: pasaron.&lt;br&gt;
Registro 3: marcado — múltiples cuentas desde una IP, alta velocidad de subred — permitido, registrado.&lt;br&gt;
Registro 4: bloqueado — umbral de velocidad superado.&lt;br&gt;
Registros 5 al 15: bloqueados al instante — "IP previamente bloqueada". La barrera recordó. Cada intento posterior murió sin siquiera ser evaluado de nuevo.&lt;br&gt;
Un bot que quería cincuenta mil cuentas consiguió cuatro. Y la persona que se registró primero nunca notó que pasara nada.&lt;/p&gt;

&lt;p&gt;Limitaciones honestas&lt;/p&gt;

&lt;p&gt;Mi prueba fue desde una sola IP. Un adversario real rota por proxies residenciales, y la velocidad desde la misma IP se debilita frente a eso. La velocidad por subred atrapa la versión perezosa; una botnet bien distribuida es un problema más difícil, y no pretendo haberlo cerrado.&lt;br&gt;
Las señales que quedan son delgadas por diseño. Nombre de usuario, IP, agente de usuario, tiempo — ese es todo el conjunto cuando te niegas a recoger identidad. Considero que el intercambio vale la pena, pero es un intercambio, y quien te diga que puede reemplazar por completo la reputación de correo con cuatro campos te está vendiendo algo.&lt;br&gt;
Los umbrales se quedan privados. Ya escribí antes sobre por qué los mensajes de rechazo nunca deben enseñarle al atacante qué lo delató; la misma regla aplica aquí. Un registro bloqueado ve "temporalmente no disponible", no qué capa se activó ni con qué conteo.&lt;br&gt;
Esto es una plataforma, con días en producción. La evidencia son mis propios logs, fechados, de mi propio servidor. No es un estudio a escala de flota.&lt;br&gt;
Por qué el punto era comernos nuestra propia comida&lt;/p&gt;

&lt;p&gt;La propuesta de Sentinel Signup a los administradores de Mastodon siempre ha sido "construido para nosotros, disponible para ti" — los módulos se extrajeron de las defensas de la propia plataforma. Desde esta semana eso es cierto en el sentido más pleno: el mismo motor de filtrado, la misma API, el mismo sistema de inquilinos que recibe cualquier cliente es lo que está entre mi propio endpoint de registro y la próxima ola de creación masiva. El cliente cero soy yo. Cuando se rompa, se me rompe a mí primero — que es exactamente el arreglo que mantiene honesto a un producto de seguridad.&lt;/p&gt;

&lt;p&gt;Si administras una plataforma con un flujo de registro —especialmente una que recoge menos de lo que la industria asume que deberías— Sentinel Signup es gratis durante la beta en signup.candortheopenfeednetwork.com, y el soporte sin datos personales descrito aquí está activo para todos los inquilinos. Si crees que mi postura ante fallos está equivocada, quiero escucharlo especialmente: &lt;a href="mailto:tips@candortheopenfeednetwork.com"&gt;tips@candortheopenfeednetwork.com&lt;/a&gt;. Respondo todo yo mismo.&lt;/p&gt;

</description>
      <category>security</category>
      <category>discuss</category>
      <category>claude</category>
      <category>architecture</category>
    </item>
    <item>
      <title>La Compatibilidad Que No Me Había Ganado</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Thu, 23 Jul 2026 20:28:34 +0000</pubDate>
      <link>https://dev.to/candornetwork/la-compatibilidad-que-no-me-habia-ganado-gan</link>
      <guid>https://dev.to/candornetwork/la-compatibilidad-que-no-me-habia-ganado-gan</guid>
      <description>&lt;p&gt;Por Ronny Cruz, fundador de Open Feed Network (Candor Network). Fundador solo, una plataforma en producción y —desde esta semana— una instancia muy pequeña del Fediverso.&lt;/p&gt;

&lt;p&gt;La línea en mi propio README&lt;/p&gt;

&lt;p&gt;Sentinel Signup es un servicio de filtrado de registros para instancias del Fediverso. Lee tu cola de solicitudes pendientes, evalúa a cada solicitante, y aprueba o rechaza a través de la API de administración. Lo lancé durante la ola de spam de julio, cuando los administradores de instancias se estaban ahogando en solicitudes falsas.&lt;/p&gt;

&lt;p&gt;Esta era la instrucción de configuración que envié con él:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Mastodon: Preferencias → Desarrollo → Nueva aplicación, permisos &lt;code&gt;admin:read admin:write&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Léelo otra vez. El producto dice &lt;em&gt;Fediverso&lt;/em&gt;. La documentación dice &lt;em&gt;Mastodon&lt;/em&gt;. No son la misma palabra, y la distancia entre ambas era una afirmación para la que no tenía evidencia.&lt;/p&gt;

&lt;p&gt;El Fediverso no es un solo programa. Es Mastodon y GoToSocial y Akkoma y Pleroma y Misskey y Sharkey e Iceshrimp y una docena más, la mayoría de los cuales implementan "la API cliente de Mastodon" en grados variables y no documentados. Mi sidecar hablaba cuatro endpoints de esa API. Los había verificado contra exactamente cero servidores, porque nunca en mi vida había administrado una instancia.&lt;/p&gt;

&lt;p&gt;Estaba vendiendo herramientas anti-abuso a una comunidad de la que no era miembro. No es un fallo moral, pero sí es uno epistémico: todo lo que sabía sobre cómo se comportan esos servidores lo sabía por leer especificaciones. Las especificaciones describen intenciones. Los despliegues tienen opiniones.&lt;/p&gt;

&lt;p&gt;Así que construí el banco de pruebas que debí haber tenido primero&lt;/p&gt;

&lt;p&gt;La restricción era que no podía costar nada. Mastodon es un inquilino pesado —Ruby, Sidekiq, Redis, su propio Postgres— y la máquina donde lo pondría ya corre cinco servicios en producción. GoToSocial es un solo binario en Go con SQLite que reposa en un par de cientos de megabytes, e implementa la API cliente de Mastodon. Esa es justo la razón por la que era la elección correcta: &lt;em&gt;no&lt;/em&gt; es Mastodon, lo que lo convierte en una prueba real y no en un ensayo.&lt;/p&gt;

&lt;p&gt;Dos cosas salieron mal antes de que llegara a arrancar, y ambas vale la pena anotarlas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La CPU se negó.&lt;/strong&gt; La compilación estándar de GoToSocial ejecuta su ffmpeg y su SQLite integrados a través de wazero, un runtime de WebAssembly, y el compilador de wazero requiere instrucciones x86-64-v2. Mi VPS se reporta como una CPU virtual genérica de QEMU —sin SSE4.2, sin SSSE3, sin POPCNT— porque eso es lo que anuncian muchos hosts virtualizados económicos o antiguos. GoToSocial no degrada a modo intérprete; lanza un pánico a propósito, cosa que respeto. La solución es una compilación oficial &lt;code&gt;nowasm&lt;/code&gt; que usa el ffmpeg nativo del sistema. Si te autoalojas en virtualización barata, revisa &lt;code&gt;/proc/cpuinfo&lt;/code&gt; antes de asumir instrucciones modernas. La mía llevaba meses siendo silenciosamente antigua.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El proxy mintió sobre quién tocaba la puerta.&lt;/strong&gt; La instancia está detrás de Cloudflare, y mi configuración de nginx reenviaba la dirección del cliente de la forma convencional. Detrás de un CDN, esa dirección es la del CDN — cada registro del planeta habría llegado desde el mismo puñado de IPs de borde.&lt;/p&gt;

&lt;p&gt;Ese merece detenerse, por &lt;em&gt;cómo&lt;/em&gt; falla. Nada da error. El servicio de filtrado sigue devolviendo veredictos. Los logs siguen viéndose sanos. Simplemente se vuelve ciego en exactamente la dimensión donde más vale: reputación de IP, velocidad por subred, detección de ráfagas — todas las señales que atrapan una granja de bots colapsan en "una dirección, siempre". Un sistema de seguridad que reporta éxito mientras no mide nada es peor que uno que está obviamente roto, lección que sigo reaprendiendo con disfraces nuevos. La solución fue una directiva de nginx leyendo la cabecera de cliente real del CDN. El hábito es: después de cualquier cambio de infraestructura, verifica que tus señales sigan llevando información, no solo que tus peticiones sigan devolviendo 200.&lt;/p&gt;

&lt;p&gt;Cómo se vio la verificación de verdad&lt;/p&gt;

&lt;p&gt;Con el nodo corriendo y accesible, revisé las cuatro llamadas que hace mi sidecar, una por una, contra un servidor que nunca había probado:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v2/admin/accounts&lt;/code&gt; — 200&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v1/admin/accounts&lt;/code&gt; (la ruta de respaldo) — 200&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v2/instance&lt;/code&gt; — 200&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/admin/accounts/{id}/reject&lt;/code&gt; — 200&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Luego la pregunta que realmente determinaba si el producto sirve de algo en GoToSocial: &lt;strong&gt;¿expone la API de administración la IP del registro?&lt;/strong&gt; Mastodon sí. Si GoToSocial no lo hiciera, entonces en instancias GTS Sentinel estaría funcionando solo con el nombre de usuario y el texto de la solicitud — algo, pero una fracción de la defensa, y tendría que decirlo en voz alta en la documentación.&lt;/p&gt;

&lt;p&gt;Sí la expone. El campo &lt;code&gt;ip&lt;/code&gt; volvió con valor. Velocidad, subred y reputación funcionan. La afirmación de compatibilidad sobrevive sin matices y —esto es lo que importa— sobrevive porque la ejecuté, no porque la asumí.&lt;/p&gt;

&lt;p&gt;También encontré dos diferencias que vale documentar. GoToSocial coloca una solicitud en la cola pendiente de inmediato, sin esperar confirmación de correo, cosa que Mastodon no hace. Y su CLI de administración no tiene &lt;code&gt;delete&lt;/code&gt; para cuentas en absoluto — solo &lt;code&gt;disable&lt;/code&gt;, más el rechazo vía API. Ninguna de las dos rompe nada. Ambas habrían sido sorpresas frente a un cliente.&lt;/p&gt;

&lt;p&gt;El primer registro real&lt;/p&gt;

&lt;p&gt;Después registré una cuenta desechable en mi propia instancia, en una ventana de incógnito, como lo haría un desconocido, y escribí una frase genuina en el campo de motivo.&lt;/p&gt;

&lt;p&gt;Treinta segundos más tarde, el sidecar la sacó de la cola, la envió al servicio de filtrado en vivo y registró:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;APROBARÍA: @[redactado] veredicto=pass confianza=1 — Historial de IP limpio y lenguaje de solicitud natural.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Modo de prueba, así que no hizo nada, que es exactamente para lo que sirve el modo de prueba. Pero la cadena completa había corrido por primera vez en su vida: solicitud pendiente → API de administración → motor de filtrado → veredicto → decisión de acción. Cada pieza que había construido por separado, funcionando junta, en un tipo de servidor que nunca había tocado.&lt;/p&gt;

&lt;p&gt;Rechazar la cuenta de prueba después devolvió 200 y dejó la cola en cero, lo que de paso verificó el último endpoint.&lt;/p&gt;

&lt;p&gt;Lo dejé en modo de prueba, a propósito&lt;/p&gt;

&lt;p&gt;El siguiente movimiento obvio es activarlo para que mi propia instancia se filtre automáticamente. No lo hice.&lt;/p&gt;

&lt;p&gt;Mi instancia no es una comunidad — es una cuenta y un banco de pruebas. Encender la aprobación automática significa que desconocidos consiguen cuentas, lo que significa que estoy alojando contenido federado, lo que significa que le he añadido una superficie de moderación a una máquina que ya corre cinco servicios en producción. Esa es una decisión real con obligaciones reales, no una bandera de configuración, y no la voy a tomar como efecto secundario de probar algo.&lt;/p&gt;

&lt;p&gt;Así que el sidecar corre continuamente en modo de prueba: cada registro futuro se filtra y se registra, dándome las observaciones, mientras la aprobación sigue siendo una decisión humana. El valor del banco de pruebas está completo. La obligación no se asume por accidente. Puedo activarlo cuando lo decida de verdad.&lt;/p&gt;

&lt;p&gt;Limitaciones honestas&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Una instancia, un registro, mi propia IP.&lt;/strong&gt; Esto prueba que la tubería conecta. No es una prueba de carga, ni una prueba adversaria, ni evidencia sobre precisión. Nadie ha atacado esta instancia, porque nadie sabe todavía que existe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una versión.&lt;/strong&gt; GoToSocial 0.22.1. La cobertura de la API se mueve entre versiones; una afirmación verificada hoy es una afirmación sobre hoy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Compatible con el Fediverso" sigue siendo excesivo, y lo retiro.&lt;/strong&gt; Lo que ahora puedo decir es: Mastodon y GoToSocial, verificados. Akkoma, Pleroma, Misskey, Sharkey, Iceshrimp — sin probar. Si administras alguno de esos y quieres averiguarlo conmigo, hago el trabajo contigo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La compilación &lt;code&gt;nowasm&lt;/code&gt; no tiene soporte oficial&lt;/strong&gt; para procesamiento de medios. Bien para un banco de pruebas. Vale saberlo antes de ponerla bajo una comunidad real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uso herramientas de IA intensivamente&lt;/strong&gt; —Claude, sobre todo— para construir y para escribir, incluido este texto. El motor de filtrado en sí es deliberadamente heurístico y basado en reglas, sin modelo en el ciclo, porque tiene que funcionar a un costo marginal efectivamente cero para instancias que no pueden pagar por protección contra spam. Donde las máquinas ayudaron, ayudaron; de donde viene la evidencia, son los logs de mi propio servidor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El punto de ser el cliente cero&lt;/p&gt;

&lt;p&gt;"Construido para nosotros, disponible para ti" ha sido la propuesta desde que los módulos de seguridad se extrajeron de mi propia plataforma. Esta semana se volvió más literal: el registro de mi plataforma está filtrado por este producto, y ahora un segundo tipo de servidor también, sobre infraestructura que yo administro.&lt;/p&gt;

&lt;p&gt;La razón por la que eso importa no es de mercadeo. Es que cada hueco que acabo de describir —la CPU, las IPs cegadas, el comportamiento de la cola, el verbo faltante en la CLI— lo habría encontrado, de otro modo, un administrador bajo ataque, en el peor momento posible, en un sistema que había confiado por mi palabra. Encontrarlos me costó una tarde y cero dólares. Encontrarlos del otro modo le cuesta a alguien su instancia.&lt;/p&gt;

&lt;p&gt;Si administras una instancia del Fediverso de cualquier sabor y tu cola de registros está llena de basura, Sentinel Signup es gratis durante la beta: signup.candortheopenfeednetwork.com. Si corres algo que no he verificado, dímelo y lo probamos como se debe — &lt;a href="mailto:tips@candortheopenfeednetwork.com"&gt;tips@candortheopenfeednetwork.com&lt;/a&gt;. Respondo todo yo mismo.&lt;/p&gt;

</description>
      <category>espanol</category>
      <category>ai</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Compatibility Claim I Hadn't Earned</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Thu, 23 Jul 2026 20:20:05 +0000</pubDate>
      <link>https://dev.to/candornetwork/the-compatibility-claim-i-hadnt-earned-32on</link>
      <guid>https://dev.to/candornetwork/the-compatibility-claim-i-hadnt-earned-32on</guid>
      <description>&lt;p&gt;The Compatibility Claim I Hadn't Earned&lt;/p&gt;

&lt;p&gt;By Ronny Cruz Alvarez, founder of Open Feed Network (Candor Network). Solo founder, one production platform, and — as of this week — one very small Fediverse instance.&lt;/p&gt;

&lt;p&gt;The line in my own README&lt;/p&gt;

&lt;p&gt;Sentinel Signup is a registration-screening service for Fediverse instances. It reads your pending signup queue, scores each applicant, and approves or rejects through the admin API. I launched it during the July spam wave, when instance admins were drowning in fake approval requests.&lt;/p&gt;

&lt;p&gt;Here's the setup instruction I shipped with it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Mastodon: Preferences → Development → New application, scopes &lt;code&gt;admin:read admin:write&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that again. The product says &lt;em&gt;Fediverse&lt;/em&gt;. The documentation says &lt;em&gt;Mastodon&lt;/em&gt;. Those are not the same word, and the gap between them was a claim I had no evidence for.&lt;/p&gt;

&lt;p&gt;The Fediverse is not one piece of software. It's Mastodon and GoToSocial and Akkoma and Pleroma and Misskey and Sharkey and Iceshrimp and a dozen others, most of which implement "the Mastodon client API" to varying and undocumented degrees. My sidecar spoke four endpoints of that API. I had verified them against exactly zero servers, because I had never run an instance in my life.&lt;/p&gt;

&lt;p&gt;I was selling anti-abuse tooling to a community I wasn't a member of. That's not a moral failing, but it is an epistemic one: everything I knew about how those servers behave, I knew from reading specifications. Specifications describe intent. Deployments have opinions.&lt;/p&gt;

&lt;p&gt;So I built the testbed I should have had first&lt;/p&gt;

&lt;p&gt;The constraint was that it had to cost nothing. Mastodon itself is a heavy tenant — Ruby, Sidekiq, Redis, its own Postgres — and the box I'd be putting it on already runs five production services. GoToSocial is a single Go binary with SQLite that idles in a couple hundred megabytes, and it implements the Mastodon client API. That's the whole reason it was the right choice: it's &lt;em&gt;not&lt;/em&gt; Mastodon, which makes it a real test rather than a rehearsal.&lt;/p&gt;

&lt;p&gt;Two things went wrong before it ever started, and both are worth writing down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CPU refused.&lt;/strong&gt; GoToSocial's standard build runs its bundled ffmpeg and SQLite through wazero, a WebAssembly runtime, and wazero's compiler requires x86-64-v2 instructions. My VPS reports itself as a generic QEMU virtual CPU — no SSE4.2, no SSSE3, no POPCNT — because that's what many budget and older virtualized hosts advertise. GoToSocial doesn't degrade to interpreter mode; it panics on purpose, which I respect. The fix is an official &lt;code&gt;nowasm&lt;/code&gt; build that uses the system's native ffmpeg instead. If you're self-hosting anything on inexpensive virtualization, check &lt;code&gt;/proc/cpuinfo&lt;/code&gt; before you assume modern instruction sets. Mine had been quietly ancient for months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The proxy lied about who was knocking.&lt;/strong&gt; The instance sits behind Cloudflare, and my nginx config forwarded the client address the conventional way. Behind a CDN, that address is the CDN's — every signup on Earth would have arrived from the same handful of edge IPs.&lt;/p&gt;

&lt;p&gt;That one is worth dwelling on, because of how it fails. Nothing errors. The screening service still returns verdicts. The logs still look healthy. It just silently becomes blind in the exact dimension it's most valuable in: IP reputation, subnet velocity, burst detection — all the signals that catch a bot farm — collapse into "one address, always." A safety system that reports success while measuring nothing is worse than one that's obviously broken, which is a lesson I keep relearning in new costumes. The fix was one nginx directive reading the CDN's real-client header. The habit is: after any infrastructure change, verify that your signals still carry information, not just that your requests still return 200.&lt;/p&gt;

&lt;p&gt;What verification actually looked like&lt;/p&gt;

&lt;p&gt;With the node running and reachable, I checked the four calls my sidecar makes, one at a time, against a server I'd never tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v2/admin/accounts&lt;/code&gt; — 200&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v1/admin/accounts&lt;/code&gt; (the fallback path) — 200&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v2/instance&lt;/code&gt; — 200&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/admin/accounts/{id}/reject&lt;/code&gt; — 200&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the question that actually determined whether the product is useful on GoToSocial at all: &lt;strong&gt;does the admin API expose the signup IP?&lt;/strong&gt; Mastodon does. If GoToSocial didn't, then on GTS instances Sentinel would be running on username and application text alone — still something, but a fraction of the defense, and I'd have to say so out loud in the documentation.&lt;/p&gt;

&lt;p&gt;It does. The &lt;code&gt;ip&lt;/code&gt; field came back populated. Velocity, subnet, and reputation all function. The compatibility claim survives without a caveat, and — this is the part that matters — it survives because I ran it, not because I assumed it.&lt;/p&gt;

&lt;p&gt;I also found two differences worth documenting. GoToSocial places a signup into the pending queue immediately, without waiting for email confirmation, which Mastodon doesn't. And its admin CLI has no &lt;code&gt;delete&lt;/code&gt; for accounts at all — only &lt;code&gt;disable&lt;/code&gt;, plus rejection through the API. Neither breaks anything. Both would have been surprises in front of a customer.&lt;/p&gt;

&lt;p&gt;The first real signup&lt;/p&gt;

&lt;p&gt;Then I registered a throwaway account on my own instance, in an incognito window, like a stranger would, and wrote a genuine sentence in the reason field.&lt;/p&gt;

&lt;p&gt;Thirty seconds later the sidecar picked it out of the queue, sent it to the live screening service, and logged:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WOULD APPROVE: @[redacted] verdict=pass confidence=1 — Clean IP history and natural application language.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Dry run, so it did nothing, which is exactly what dry run is for. But the whole chain had run for the first time in its life: pending signup → admin API → screening engine → verdict → action decision. Every piece I'd built separately, working together, on a server type I'd never touched.&lt;/p&gt;

&lt;p&gt;Rejecting the test account afterward returned 200 and cleared the queue to zero, which incidentally verified the last endpoint.&lt;/p&gt;

&lt;p&gt;I left it in dry run, on purpose&lt;/p&gt;

&lt;p&gt;The obvious next move is flipping it live so my own instance is auto-screened. I didn't.&lt;/p&gt;

&lt;p&gt;My instance isn't a community — it's one account and a testbed. Turning on auto-approval means strangers get accounts, which means I'm hosting federated content, which means I've added a moderation surface to a machine already running five production services. That's a real decision with real obligations, not a config flag, and I'm not making it as a side effect of testing something.&lt;/p&gt;

&lt;p&gt;So the sidecar runs continuously in dry run: every future signup gets screened and logged, giving me the observations, while approval stays a human decision. The testbed value is fully there. The obligation isn't taken on accidentally. I can flip it whenever I mean to.&lt;/p&gt;

&lt;p&gt;Honest limitations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One instance, one signup, my own IP.&lt;/strong&gt; This proves the plumbing connects. It is not a load test, not an adversarial test, and not evidence about accuracy. Nobody has attacked this instance, because nobody knows it exists yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One version.&lt;/strong&gt; GoToSocial 0.22.1. API coverage moves between releases; a claim verified today is a claim about today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Fediverse-compatible" is still an overclaim, and I'm retiring it.&lt;/strong&gt; What I can now say is: Mastodon and GoToSocial, verified. Akkoma, Pleroma, Misskey, Sharkey, Iceshrimp — untested. If you run one of those and want to find out with me, I'll do the work with you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;nowasm&lt;/code&gt; build is officially unsupported&lt;/strong&gt; for media processing. Fine for a testbed. Worth knowing before you put it under a real community.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I use AI tooling heavily&lt;/strong&gt; — Claude, mostly — to build and to write, including this post. The screening engine itself is deliberately heuristic and rule-based with no model in the loop, because it has to run at effectively zero marginal cost for instances that can't pay for spam protection. Where the machines helped, they helped; where the evidence comes from, it's my own server's logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point of being customer zero&lt;/p&gt;

&lt;p&gt;"Built for ourselves, available to you" has been the pitch since the safety modules were extracted from my own platform. This week it got more literal: my platform's registration is screened by this product, and now a second server type is too, on infrastructure I run.&lt;/p&gt;

&lt;p&gt;The reason that matters isn't marketing. It's that every gap I just described — the CPU, the blinded IPs, the queue behavior, the missing CLI verb — would otherwise have been found by an admin under attack, at the worst possible moment, in a system they'd trusted on my word. Finding them cost me an evening and zero dollars. Finding them the other way costs someone their instance.&lt;/p&gt;

&lt;p&gt;If you run a Fediverse instance of any flavor and your signup queue is full of garbage, Sentinel Signup is free during beta: signup.candortheopenfeednetwork.com. If you run something I haven't verified, tell me and let's test it properly — &lt;a href="mailto:tips@candortheopenfeednetwork.com"&gt;tips@candortheopenfeednetwork.com&lt;/a&gt;. I answer everything myself.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Signup Form With Nothing to Filter</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Thu, 23 Jul 2026 20:18:34 +0000</pubDate>
      <link>https://dev.to/candornetwork/the-signup-form-with-nothing-to-filter-5d3k</link>
      <guid>https://dev.to/candornetwork/the-signup-form-with-nothing-to-filter-5d3k</guid>
      <description>&lt;p&gt;By Ronny Cruz, founder of Open Feed Network (Candor Network). Solo founder, one production platform, and a registration flow that deliberately knows almost nothing about you.&lt;/p&gt;

&lt;p&gt;The problem I built for myself&lt;/p&gt;

&lt;p&gt;Candor's registration collects no email. No phone number. No CAPTCHA account, no OAuth identity, nothing to verify because there is nothing to verify against. You install the app, choose a username, set a PIN, and the browser generates an ECDSA P-256 keypair on your device. The server receives exactly two things: the username and the public key. Twelve seed words and a recovery file stay with you. That's the entire signup.&lt;/p&gt;

&lt;p&gt;I built it that way on purpose. Zero-PII registration means there is no email database to breach, no phone list to subpoena, no identity graph to sell. For a platform whose whole thesis is that your voice shouldn't depend on trusting me, it's the right design.&lt;/p&gt;

&lt;p&gt;It is also, from a spam-defense perspective, a nightmare I handed to myself. Every signal the industry leans on for signup screening — disposable email domains, email reputation, verification round-trips — assumes an email exists. Mine doesn't. And the flow is fast by design, which means a script can be fast too. Last Friday an AI code review put it bluntly: nothing stopped a bot from looping the registration endpoint and minting fifty thousand accounts, flooding the feed with identities that cost nothing to create.&lt;/p&gt;

&lt;p&gt;The review was right. Nothing did.&lt;/p&gt;

&lt;p&gt;The irony was that I sell the fix&lt;/p&gt;

&lt;p&gt;The week before, I had launched Sentinel Signup — a registration-screening service for Fediverse instances getting hammered by exactly this kind of wave. Six layers: IP and domain reputation, subnet velocity, timing patterns, content analysis, the works. Built for Mastodon admins drowning in fake approval requests.&lt;/p&gt;

&lt;p&gt;My own platform wasn't using it. The cobbler's children, barefoot as usual.&lt;/p&gt;

&lt;p&gt;Worse: it &lt;em&gt;couldn't&lt;/em&gt; use it. The screening API required an email in every request, because every customer I'd imagined had one. My own registration flow — the most privacy-preserving one I know of — was structurally locked out of my own security product. And the engine went further than requiring email: an absent email scored a +30 "invalid email" penalty. Run my own honest users through my own filter, and every single one would start out looking suspicious. A privacy feature read as a fraud signal.&lt;/p&gt;

&lt;p&gt;That's worth sitting with, because it isn't a quirk of my code. It's what happens across this industry when anti-abuse systems assume identity signals that privacy-respecting platforms deliberately don't collect. The default posture of spam defense is that knowing less about a user makes them more suspect. If we want privacy-preserving platforms to exist, the security tooling has to stop punishing them for the privacy.&lt;/p&gt;

&lt;p&gt;The fix: absence is not malformation&lt;/p&gt;

&lt;p&gt;The change was small and philosophically important. The engine now distinguishes two things it previously conflated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email provided but malformed&lt;/strong&gt; — still penalized. Someone sent garbage in an email field; that's a real signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email not provided at all&lt;/strong&gt; — recorded as a neutral fact, weight zero. A zero-PII platform has nothing to send, and honesty about that costs its users nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every check that fires when an email &lt;em&gt;is&lt;/em&gt; present — disposable-domain lists, throwaway-pattern matching — fires exactly as before. The test suite proves both directions: no penalty for absence, no softening for garbage.&lt;/p&gt;

&lt;p&gt;With that, my registration could finally be screened by my own product, using only what genuinely exists: username, real client IP (from the CDN's forwarded header — the service itself never sees raw connections), user agent, and time. No email, no synthetic placeholder polluting the analysis. What's real gets analyzed; what doesn't exist isn't faked.&lt;/p&gt;

&lt;p&gt;The posture question nobody asks until an outage&lt;/p&gt;

&lt;p&gt;Wiring the screen into the registration handler forced a decision most integrations skip: what happens when screening itself fails?&lt;/p&gt;

&lt;p&gt;For my Mastodon-admin customers, the answer is fail-to-queue — a screening error routes the applicant to the human approval queue, where a moderator decides. Never silently admitted, never silently rejected.&lt;/p&gt;

&lt;p&gt;My registration flow has no queue. There is no moderator between a human and their account, by design. So fail-to-queue is meaningless here, and the two textbook options are both wrong: fail-closed means a two-second screening hiccup locks real humans out of joining the platform; fail-open with silence means bots stream through and nobody knows.&lt;/p&gt;

&lt;p&gt;The posture I shipped: &lt;strong&gt;block only on a hard block verdict; on a flag or any error, allow — loudly.&lt;/strong&gt; Every flagged registration and every screening failure lands in the logs with the reason. A burst that slips through during an outage is visible and attributable within minutes. A human locked out by a dependency failure would be invisible forever — they'd just leave. Between those two failure costs, the choice isn't close.&lt;/p&gt;

&lt;p&gt;What the gate did to the bot scenario&lt;/p&gt;

&lt;p&gt;After deploying, I ran the attack against my own production server. First, one legitimate registration — passed clean, token issued, indistinguishable from before. Then the miniature version of the fifty-thousand-account scenario: rapid-fire registrations from a single IP, fresh keypair each time.&lt;/p&gt;

&lt;p&gt;The escalation ladder, straight from the logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registrations 1–2: passed.&lt;/li&gt;
&lt;li&gt;Registration 3: &lt;strong&gt;flagged&lt;/strong&gt; — multiple accounts from one IP, high subnet velocity — allowed, logged.&lt;/li&gt;
&lt;li&gt;Registration 4: &lt;strong&gt;blocked&lt;/strong&gt; — velocity threshold crossed.&lt;/li&gt;
&lt;li&gt;Registrations 5 through 15: &lt;strong&gt;blocked instantly&lt;/strong&gt; — "previously blocked IP." The gate remembered. Every subsequent attempt died without even being re-scored.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A bot that wanted fifty thousand accounts got four. And the human who registered first never noticed anything happened at all.&lt;/p&gt;

&lt;p&gt;Honest limitations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My test was a single IP.&lt;/strong&gt; A real adversary rotates through residential proxies, and same-IP velocity weakens against that. Subnet velocity catches the lazy version; a well-distributed botnet is a harder problem, and I'm not claiming this closes it. Rate-limiting keypair minting per network is a floor, not a ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The remaining signals are thin by design.&lt;/strong&gt; Username, IP, user agent, timing — that's the whole feature set when you refuse to collect identity. I consider that trade worth it, but it's a trade, and anyone telling you they can fully replace email reputation with four fields is selling something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thresholds stay private.&lt;/strong&gt; I've written before about why rejection messages must never teach attackers what tripped them; the same rule applies here. A blocked registration sees "temporarily unavailable," not which layer fired or at what count. That's deliberate, and it means this article tells you the shape of the defense, not its numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is one platform, days old in production.&lt;/strong&gt; The evidence is my own logs, dated, from my own server. It is not a fleet-scale study.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why dogfooding was the point&lt;/p&gt;

&lt;p&gt;Sentinel Signup's pitch to Mastodon admins has been "built for ourselves, available to you" — the modules were extracted from the platform's own defenses. As of this week that's true in the fullest sense: the same screening engine, the same API, the same tenant system any customer gets is what stands between my own registration endpoint and the next minting run. Customer zero is me. When it breaks, I'm the first one it breaks on — which is exactly the arrangement that keeps a safety product honest.&lt;/p&gt;

&lt;p&gt;If you run a platform with a registration flow — especially one that collects less than the industry assumes you should — Sentinel Signup is free during beta at signup.candortheopenfeednetwork.com, and the zero-PII support described here is live for every tenant. If you think my failure posture is wrong, I especially want to hear that: &lt;a href="mailto:tips@candortheopenfeednetwork.com"&gt;tips@candortheopenfeednetwork.com&lt;/a&gt;. I answer everything myself.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Social Network That Was Built Safety-First</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:22:05 +0000</pubDate>
      <link>https://dev.to/candornetwork/the-social-network-that-was-built-safety-first-3a6i</link>
      <guid>https://dev.to/candornetwork/the-social-network-that-was-built-safety-first-3a6i</guid>
      <description>&lt;p&gt;&lt;em&gt;How Candor: The Open Feed Network rethinks what a platform owes the people on it&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most social platforms were built the same way: grow first, moderate later. Safety teams arrived years after the feed did, bolted onto architectures that were never designed to protect anyone. Candor: The Open Feed Network was built in the opposite order — and that inversion shapes everything about it.&lt;/p&gt;

&lt;p&gt;Candor, which entered public beta on July 4, 2026, is a bilingual English/Spanish social feed and livestreaming platform created by founder Ronny Cruz of Open Feed Network, Inc. Its core feed is open source under the MIT license, published for anyone to inspect. What makes it unusual is what runs underneath: a suite of purpose-built safety systems that screen content before it ever reaches another user — not after someone reports it.&lt;/p&gt;

&lt;h2&gt;
  
  
  No email. No password. No profile to sell.
&lt;/h2&gt;

&lt;p&gt;Candor's most radical choice is what it refuses to collect. There is no email signup, no password, no phone number. Instead, identity is cryptographic: when you join, your device generates a keypair secured by a recovery seed phrase, and a PIN-protected vault that never leaves your own device. The server never sees your private key and holds no personal information about you — because it was never given any.&lt;/p&gt;

&lt;p&gt;This isn't privacy as a settings toggle. It's privacy as architecture: the platform cannot leak, sell, or be compelled to hand over data it does not possess.&lt;/p&gt;

&lt;p&gt;The same principle governs where content lives. User posts are hosted in Iceland under GDPR, one of the strongest data-protection regimes in the world, while legally mandated safety reporting runs through U.S. infrastructure — a deliberate split that keeps user data under strong privacy law without compromising the platform's legal obligations to protect people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safety that runs before the harm, not after
&lt;/h2&gt;

&lt;p&gt;The safety layer is where Candor departs most sharply from the industry norm. Every piece of content passes through screening at the moment it enters the platform. Candor is an approved reporting partner of the National Center for Missing &amp;amp; Exploited Children and a licensed user of Microsoft's PhotoDNA, which blocks known child sexual abuse material at upload — the gate fails closed, meaning if screening can't run, the content doesn't post.&lt;/p&gt;

&lt;p&gt;Livestreams get the same treatment in real time. Candor's live pipeline transcribes streams as they happen and scores them for terrorist and violent extremist content within seconds — not minutes, not after a viewer files a report. It is an answer, in working code, to the question the industry has struggled with since the Christchurch attack was broadcast live in 2019: how do you stop a livestream being used as a weapon?&lt;/p&gt;

&lt;p&gt;Alongside threat detection runs something rarer: care. Candor Beacon monitors for signs of mental-health crisis — and it's built to tell the difference between dark humor, ordinary venting, and genuine distress, responding only when someone may actually need help. The platform's own testing philosophy is blunt about the stakes: a system that cries wolf is as useless as one that stays silent.&lt;/p&gt;

&lt;p&gt;A third layer, Truth Shield, is free and open source: an AI-powered disinformation detector that labels posts with a risk assessment readers can see, rather than silently deciding what they're allowed to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Built for ourselves, available to you"
&lt;/h2&gt;

&lt;p&gt;Every one of these systems was built first to protect Candor's own feed. Only afterward were they extracted as independent services that other platforms — community sites, streaming services, youth platforms — can license and run themselves. That ordering matters: these aren't compliance products designed to a sales deck. They're the actual defenses of a working platform, offered to anyone else who needs them.&lt;/p&gt;

&lt;p&gt;It's a small operation making a large argument. Candor is built and run by a solo founder from Puerto Rico, operating infrastructure across three countries, screening two languages in real time. If one person building safety-first can do this, the argument goes, the platforms with thousands of engineers have run out of excuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Candor is betting on
&lt;/h2&gt;

&lt;p&gt;Candor's wager is that people are tired of the trade they've been offered — connection in exchange for surveillance, reach in exchange for safety. Its answer is a feed where you are not the product, where protection runs in the pipeline rather than the press release, and where the code making those promises is public enough to check.&lt;/p&gt;

&lt;p&gt;The platform is live now at candortheopenfeednetwork.com. Bring nothing but a seed phrase.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>architecture</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Casi le Enseñamos a los Bots de Spam Cómo Vencernos</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Mon, 20 Jul 2026 18:53:38 +0000</pubDate>
      <link>https://dev.to/candornetwork/casi-le-ensenamos-a-los-bots-de-spam-como-vencernos-j54</link>
      <guid>https://dev.to/candornetwork/casi-le-ensenamos-a-los-bots-de-spam-como-vencernos-j54</guid>
      <description>&lt;p&gt;Por Ronny Cruz, fundador de Open Feed Network (Candor Network). Fundador solo, una plataforma en producción, muchas opiniones sobre lo que un mensaje de rechazo tiene permitido decir.&lt;/p&gt;

&lt;p&gt;El bug que me hizo pensar&lt;/p&gt;

&lt;p&gt;Hace dos semanas, en medio de construir una herramienta de filtrado de registros durante la más reciente ola de spam en el Fediverso, encontré un bug en mi propio motor de detección que me enseñó algo que ya debí haber sabido: la salida de un sistema de seguridad es, en sí misma, una superficie de ataque.&lt;/p&gt;

&lt;p&gt;El motor — Sentinel, el clasificador de defensa perimetral detrás de todo lo que construyo — marca texto de registro spam usando un banco de patrones. Ofertas de criptomonedas, aperturas genéricas tipo plantilla, ese tipo de cosas. Cuando un patrón coincidía, la razón de rechazo que regresaba a quien llamaba se veía así:&lt;/p&gt;

&lt;p&gt;jsreason: spam phrase matched: /crypto|investment.{0,10}opportunity/i&lt;/p&gt;

&lt;p&gt;Legible. Fácil de depurar. Y, si ese mensaje llegaba a la persona rechazada en vez de quedarse en un log privado de administrador — una especificación completa y legible por máquina de exactamente qué palabras evitar. No construí un filtro de spam. Construí un filtro de spam con sus propias instrucciones de cómo evadirlo impresas en la caja.&lt;/p&gt;

&lt;p&gt;Nada se rompió. Nada se veía mal en las pruebas. El regex hacía su trabajo perfectamente, cada vez — mientras silenciosamente entregaba la respuesta correcta.&lt;/p&gt;

&lt;p&gt;Este no es un error raro. Es el modo de fallo normal de construir sistemas de detección rápido: instrumentas para tu propia depuración, y olvidas que "útil para mí" y "útil para un atacante" muchas veces es el mismo texto.&lt;/p&gt;

&lt;p&gt;El invariante&lt;/p&gt;

&lt;p&gt;Lo arreglé el mismo día, pero el parche importó menos que la regla que escribí después, y esa regla es la que realmente gobierna Sentinel ahora:&lt;/p&gt;

&lt;p&gt;La lógica de detección y la divulgación de la detección son sistemas distintos, y solo uno de los dos llega a quien hizo la solicitud.&lt;/p&gt;

&lt;p&gt;Internamente, cada veredicto todavía lleva su razonamiento completo — qué capa se activó, qué patrón coincidió, el peso exacto. Ese detalle es esencial; un sistema de seguridad que no puedes auditar es un sistema que no puedes confiar, ni siquiera desde adentro. Pero nada interno es el mismo objeto que lo que ve un usuario rechazado o una respuesta pública de API. La razón pública se toma de un vocabulario fijo y genérico — "dominio de correo desechable," "frase de spam detectada," "patrón de registro marcado" — deliberadamente incapaz de nombrar un regex específico, un umbral específico, o una entrada específica de una lista. Un atacante que sondea la API puede aprender que algo se activó. No puede aprender qué cambiar para dejar de activarlo.&lt;/p&gt;

&lt;p&gt;Esa distinción ahora sostiene todo lo que Sentinel hace, en los dos lugares donde corre.&lt;/p&gt;

&lt;p&gt;Dos frentes, un solo motor&lt;/p&gt;

&lt;p&gt;Sentinel empezó como defensa perimetral: puntuación a nivel de solicitud en el borde — reputación, velocidad, regularidad de tiempos — decidiendo qué se desafía o bloquea antes de que toque una aplicación. Sentinel Signup es el mismo motor de seis capas apuntando a un problema más específico: colas de registro del Fediverso, que la ola de spam de julio convirtió en una crisis real para instancias de Mastodon administradas por voluntarios. Correos desechables, IPs de centros de datos, texto tipo plantilla de "apruébame por favor," ráfagas de registro desde la misma subred, y — añadido esta semana, después de que un administrador de habla hispana describiera estar siendo golpeado por lo que parecía una ola coordinada en ruso — una señal de discordancia de escritura/idioma: texto de solicitud en una escritura que contradice el idioma declarado de la cuenta.&lt;/p&gt;

&lt;p&gt;La misma regla de divulgación aplica a ambos. El guardia perimetral le dice a un cliente desafiado "solicitud marcada." No le dice cuál de las verificaciones de reputación, velocidad, o regularidad se activó, ni por cuánto. Sentinel Signup le dice a un administrador de instancia "bloqueado: dominio de correo desechable; IP de centro de datos; contenido tipo plantilla" — categorías, no la biblioteca de patrones subyacente. Un administrador recibe suficiente para confiar en el veredicto. Un operador de bots no recibe nada para evadirlo.&lt;/p&gt;

&lt;p&gt;Falla-a-cola, no falla-abierto ni falla-cerrado&lt;/p&gt;

&lt;p&gt;La otra regla que Sentinel Signup impone sin excepción: si el filtrado mismo falla — una dependencia se cae, una consulta falla, lo que sea — el veredicto es marcar para revisión, nunca aprobar y nunca bloquear.&lt;/p&gt;

&lt;p&gt;Esa es una tercera opción deliberada que la mayoría de sistemas no construyen. Fallar-abierto en un filtro de spam significa que una caída se convierte en una puerta abierta. Fallar-cerrado significa que un percance de un martes por la tarde bloquea a los nuevos usuarios reales tan duro como a los bots — y cerrar los registros es exactamente la jugada perdedora que los administradores ya estaban haciendo antes de que esto existiera; automatizar esa pérdida no es una solución. Falla-a-cola significa que un error le cuesta a un moderador humano treinta segundos de juicio, y no le cuesta nada al solicitante. El fallo es visible, es barato, y nunca fabrica un veredicto falso en ninguna dirección.&lt;/p&gt;

&lt;p&gt;La pieza complementaria es la misma disciplina de falla-ruidosa que uso en todas partes: una conexión de base de datos faltante al arrancar es una negativa a iniciar, no una caída silenciosa a memoria que pierde silenciosamente los datos de cada instancia en el próximo reinicio. Prefiero que el servicio no arranque a que arranque mintiendo sobre lo que puede garantizar.&lt;/p&gt;

&lt;p&gt;Limitaciones honestas&lt;/p&gt;

&lt;p&gt;Las partes de esto que todavía no están terminadas:&lt;/p&gt;

&lt;p&gt;El límite de divulgación se impone hoy por convención a nivel de código — cada ruta de respuesta se revisa a mano para confirmar que toma del vocabulario genérico, no del conjunto de patrones interno. Todavía no se impone estructuralmente, de la forma en que la capa de almacenamiento del feed principal de Candor estructuralmente no puede aceptar contenido sin filtrar. Ese es el próximo paso de endurecimiento, y hasta que se construya, "solo genérico" es una disciplina que mantengo, no una garantía que la arquitectura me da.&lt;/p&gt;

&lt;p&gt;El análisis de texto hoy es heurístico y basado en reglas, más la puntuación de contenido propia del motor. No hay un clasificador aprendido en el ciclo, y no hay ningún LLM en el ciclo para el filtrado de registros — lo cual es deliberado: esto necesita funcionar a un costo marginal efectivamente cero por evaluación, ya que las instancias que no pueden pagar por protección contra spam son exactamente las que están siendo golpeadas más duro. Un clasificador opcional, revelado por instancia, está diseñado pero no construido.&lt;/p&gt;

&lt;p&gt;La señal de escritura/idioma es nueva — tiene días, un solo patrón de ataque real detrás de ella. Solo se activa cuando una cuenta declara un idioma y el texto del solicitante lo contradice de una forma específica, y una sola coincidencia gana revisión humana, nunca rechazo automático. Ese conservadurismo es intencional; prefiero que falle por defecto un tiempo a que aprenda una mala heurística con evidencia insuficiente.&lt;/p&gt;

&lt;p&gt;Soy un fundador solo. Sentinel ha estado corriendo defensa perimetral en producción desde mayo. Sentinel Signup tiene una semana, está vivo, y actualmente es gratis mientras está en beta específicamente para que tráfico adversario real — no casos de prueba sintéticos — me diga dónde realmente está equivocado antes de que alguien pague por ello.&lt;/p&gt;

&lt;p&gt;Por qué este orden importa&lt;/p&gt;

&lt;p&gt;Todo sistema de detección eventualmente es sondeado por lo mismo que detecta. La pregunta no es si un atacante enviará tráfico de prueba y leerá tus respuestas — lo hará, inmediatamente, así es como descubrió que tu filtro existía. La pregunta es si tu respuesta le enseña algo. Un mensaje de rechazo no es un log privado de depuración. En el momento en que sale de tu proceso, es el reconocimiento gratuito más valioso que le puedes dar a la persona que construiste el sistema para detener.&lt;/p&gt;

&lt;p&gt;Si administras una instancia de Mastodon, o cualquier cosa con una cola de registro que esté siendo golpeada ahora mismo, Sentinel Signup es gratis durante la beta en signup.candortheopenfeednetwork.com. Si quieres decirme dónde el límite de divulgación todavía tiene fugas — prefiero escucharlo de ti que de alguien usándolo. Estoy en &lt;a href="mailto:tips@candortheopenfeednetwork.com"&gt;tips@candortheopenfeednetwork.com&lt;/a&gt;, y respondo todo yo mismo.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>architecture</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>We Almost Told the Spam Bots How to Beat Us</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Mon, 20 Jul 2026 18:19:41 +0000</pubDate>
      <link>https://dev.to/candornetwork/we-almost-told-the-spam-bots-how-to-beat-us-5dof</link>
      <guid>https://dev.to/candornetwork/we-almost-told-the-spam-bots-how-to-beat-us-5dof</guid>
      <description>&lt;p&gt;By Ronny Cruz, founder of Open Feed Network (Candor Network). Solo founder, one production platform, a lot of opinions about what a rejection message is allowed to say.&lt;/p&gt;

&lt;p&gt;The bug that convinced me&lt;/p&gt;

&lt;p&gt;Two weeks ago, in the middle of building a registration-screening tool during the Fediverse's latest signup-spam wave, I found a bug in my own detection engine that taught me something I should have already known: a security system's output is an attack surface.&lt;/p&gt;

&lt;p&gt;The engine — Sentinel, the perimeter-defense classifier behind everything I build — flags spam signup text using a bank of pattern matchers. Cryptocurrency pitches, generic templated openers, that kind of thing. When a pattern matched, the rejection reason returned to the caller looked something like this:&lt;/p&gt;

&lt;p&gt;jsreason: &lt;code&gt;spam phrase matched: /crypto|investment.{0,10}opportunity/i&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Readable. Debuggable. And, if that message ever reached the person being rejected instead of staying in an admin's private log — a complete, machine-readable specification of exactly which words to avoid. I didn't build a spam filter. I built a spam filter with its own bypass instructions printed on the box.&lt;/p&gt;

&lt;p&gt;Nothing crashed. Nothing looked wrong in testing. The regex did its job perfectly, every time — while quietly handing out the answer key.&lt;/p&gt;

&lt;p&gt;This is not a rare mistake. It's the normal failure mode of building detection systems fast: you instrument for your own debugging, and you forget that "helpful to me" and "helpful to an attacker" are often the same string.&lt;/p&gt;

&lt;p&gt;The invariant&lt;/p&gt;

&lt;p&gt;I patched it the same day, but the patch mattered less than the rule I wrote afterward, and the rule is what actually governs Sentinel now:&lt;/p&gt;

&lt;p&gt;Detection logic and detection disclosure are different systems, and only one of them ever faces the caller.&lt;/p&gt;

&lt;p&gt;Internally, every verdict still carries its full reasoning — which layer fired, which pattern matched, the exact weight. That detail is essential; a safety system you can't audit is a safety system you can't trust, including from yourself. But nothing internal is the same object as what a rejected user or a public API response ever sees. The public reason is drawn from a fixed, generic vocabulary — "disposable email domain," "spam phrase detected," "signup pattern flagged" — deliberately incapable of naming a specific regex, a specific threshold, or a specific list entry. An attacker probing the API can learn that something tripped. They cannot learn what to change to stop tripping it.&lt;/p&gt;

&lt;p&gt;That distinction is now load-bearing across both places Sentinel runs.&lt;/p&gt;

&lt;p&gt;Two fronts, one engine&lt;/p&gt;

&lt;p&gt;Sentinel started as perimeter defense: request-level scoring at the edge — reputation, velocity, timing regularity — deciding what gets challenged or blocked before it touches an application at all. Sentinel Signup is the same six-layer engine pointed at a narrower, more specific problem: Fediverse registration queues, which the July spam wave turned into a genuine crisis for volunteer-run Mastodon instances. Disposable emails, datacenter IPs, templated "please approve me" text, same-subnet signup bursts, and — added this week, after a Spanish-language admin described being hit by what looked like a coordinated Russian-language wave — a script/locale mismatch signal: application text in a script that contradicts the account's declared locale.&lt;/p&gt;

&lt;p&gt;Same disclosure rule applies to both. The perimeter guard tells a challenged client "request flagged." It does not tell them which of the reputation, velocity, or timing checks fired, or by how much. Sentinel Signup tells an instance admin "block: disposable email domain; datacenter IP; templated content" — categories, not the underlying pattern library. An admin gets enough to trust the verdict. A bot operator gets nothing to route around.&lt;/p&gt;

&lt;p&gt;Fail-to-queue, not fail-open or fail-closed&lt;/p&gt;

&lt;p&gt;The other rule Sentinel Signup enforces without exception: if screening itself errors — a dependency times out, a lookup fails, anything — the verdict is flag, never pass and never block.&lt;/p&gt;

&lt;p&gt;That's a deliberate third option most systems don't build. Fail-open on a spam filter means an outage becomes an open door. Fail-closed means a Tuesday-afternoon hiccup locks out real newcomers as hard as it locks out bots — and closing registrations is exactly the losing move admins were already making before any of this existed; automating that loss isn't a fix. Fail-to-queue means an error costs a human moderator thirty seconds of judgment, and costs the applicant nothing at all. The failure is visible, it's cheap, and it never manufactures a false verdict in either direction.&lt;/p&gt;

&lt;p&gt;The companion piece is the same fail-loud discipline I use everywhere: a missing database connection at boot is a refusal to start, not a silent fallback to memory that quietly loses every tenant's data on the next restart. I'd rather the service not come up than come up lying about what it can guarantee.&lt;/p&gt;

&lt;p&gt;Honest limitations&lt;/p&gt;

&lt;p&gt;The parts of this that aren't finished yet:&lt;/p&gt;

&lt;p&gt;The disclosure boundary is enforced by convention at the code layer today — every response path is hand-reviewed to confirm it draws from the generic vocabulary, not the internal pattern set. It is not yet enforced structurally, the way the storage layer in Candor's core feed structurally cannot accept unscreened content. That's the next hardening step, and until it's built, "generic-only" is a discipline I maintain, not a guarantee the architecture makes for me.&lt;/p&gt;

&lt;p&gt;Text analysis today is heuristic and rule-based, plus the underlying engine's own content scoring. There's no learned classifier in the loop, and no LLM in the loop for signup screening at all — which is deliberate: this needs to work at effectively zero marginal cost per screening, since instances that can't pay for spam protection are exactly the ones getting hit hardest. An opt-in classifier, disclosed per-instance, is designed but not built.&lt;/p&gt;

&lt;p&gt;The script/locale signal is new — days old, one real attack pattern behind it. It fires only when an account declares a locale and the applicant's text contradicts it in a specific way, and a lone match earns human review, never automatic rejection. That conservatism is intentional; I'd rather it under-fire for a while than teach itself a bad heuristic on thin evidence.&lt;/p&gt;

&lt;p&gt;I'm a solo founder. Sentinel has been running perimeter defense in production since May. Sentinel Signup is one week old, live, and currently free while in beta specifically so real adversarial traffic — not synthetic test cases — can tell me where it's actually wrong before anyone pays for it.&lt;/p&gt;

&lt;p&gt;Why this ordering matters&lt;/p&gt;

&lt;p&gt;Every detection system eventually gets probed by the thing it's detecting. The question isn't whether an attacker will send test traffic and read your responses — they will, immediately, that's how you found out your filter existed. The question is whether your response teaches them anything. A rejection message is not a private debugging log. The moment it leaves your process, it's the most valuable free reconnaissance you can hand the person you built the system to stop.&lt;/p&gt;

&lt;p&gt;If you run a Mastodon instance, or anything with a registration queue getting hit right now, Sentinel Signup is free during beta at signup.candortheopenfeednetwork.com. If you want to tell me where the disclosure boundary still leaks — I'd rather hear it from you than from someone using it. I'm at &lt;a href="mailto:tips@candortheopenfeednetwork.com"&gt;tips@candortheopenfeednetwork.com&lt;/a&gt;, and I answer everything myself.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>architecture</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Meet Channel and Zoe — The Faces of Candor Network</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Fri, 17 Jul 2026 22:00:09 +0000</pubDate>
      <link>https://dev.to/candornetwork/meet-channel-and-zoe-the-faces-of-candor-network-4758</link>
      <guid>https://dev.to/candornetwork/meet-channel-and-zoe-the-faces-of-candor-network-4758</guid>
      <description>&lt;p&gt;Every safety platform needs someone watching the door.&lt;/p&gt;

&lt;p&gt;We have two.&lt;/p&gt;

&lt;p&gt;Channel — The Guardian&lt;/p&gt;

&lt;p&gt;Channel is a dark brindle French Bulldog with a stare that could stop a Russian propaganda bot mid-registration. He doesn't bark much. He doesn't need to. He sits at the gate, watches everything that comes through, and nothing — nothing — gets past him without being screened first.&lt;/p&gt;

&lt;p&gt;In the Candor architecture, Channel represents the embedded gateway: the layer where content screening isn't bolted alongside storage as an afterthought, but fused directly into the write path itself. No content enters the feed without passing Channel first. That's not a policy. That's a structural guarantee.&lt;/p&gt;

&lt;p&gt;Role: Chief Security Officer&lt;br&gt;
Superpower: Fail-closed by default&lt;br&gt;
Vibe: Calm, serious, deeply unimpressed by your bypass attempt&lt;/p&gt;

&lt;p&gt;Zoe — The Detector&lt;/p&gt;

&lt;p&gt;Zoe is a merle French Bulldog with one brown eye and one blue wall eye. That mismatched gaze isn't a flaw — it's her whole character. One eye sees what everyone else sees. The other eye sees what's hidden.&lt;/p&gt;

&lt;p&gt;She's the one who notices the crisis signal buried in a normal-looking post. The extremism pattern in a livestream transcript. The disinformation score that's just slightly too high. The thing that doesn't throw an error, doesn't crash anything, doesn't show up in any log — but is wrong, and she knows it.&lt;/p&gt;

&lt;p&gt;In the Candor architecture, Zoe represents Beacon and the detection layer: the systems that watch the living feed for signals that matter, in English and in Spanish, because harmful content doesn't pick a language.&lt;/p&gt;

&lt;p&gt;Role: Chief Detection Officer&lt;br&gt;
Superpower: Sees what others miss — literally&lt;br&gt;
Vibe: Alert, quick, that slightly alarmed look that means she just caught something&lt;/p&gt;

&lt;p&gt;The real story&lt;/p&gt;

&lt;p&gt;Channel and Zoe are real. They live in Tampa, Florida with the solo founder building all of this — one engineer, one platform, two dogs who apparently take content moderation very seriously.&lt;/p&gt;

&lt;p&gt;The decision to make them the faces of Candor wasn't a branding exercise. It was simpler than that: I can't be in videos for legal reasons, and synthetic personas feel dishonest for a company whose name is literally Candor. Real dogs from a real household felt right. They have actual personalities. Channel really does have that stare. Zoe really does have that eye.&lt;/p&gt;

&lt;p&gt;A mascot built on something true is harder to forget than one built on a stock photo.&lt;/p&gt;

&lt;p&gt;What they're guarding&lt;/p&gt;

&lt;p&gt;Candor Network is a social platform and a suite of content-screening APIs built around one architectural invariant:&lt;/p&gt;

&lt;p&gt;The database accepts only screened content, because the database forgets nothing.&lt;/p&gt;

&lt;p&gt;The feed store is append-only and cryptographically hash-chained. Screening runs before the write — not alongside it, not after it, before it. There is no write path that bypasses the gate. What Channel guards is structurally impossible to sneak past, not just policy-prohibited.&lt;/p&gt;

&lt;p&gt;The individual screening modules — crisis detection, extremism classification, disinformation scoring, sextortion detection — are also available as standalone APIs, priced for operators who can't afford enterprise trust-and-safety tooling but can't afford to skip it either. Bilingual EN/ES by design, because the moderation industry's chronic underinvestment in Spanish isn't something we're replicating.&lt;/p&gt;

&lt;p&gt;What comes next&lt;/p&gt;

&lt;p&gt;Channel and Zoe will be here every week. Channel explains the architecture — the gateway, the hash-chain, the fail-closed matrix, the tombstone redaction. Zoe explains the detection — what the crisis classifier actually looks for, how disinformation scoring works at feed speed, what "bilingual by design" means in practice versus as a marketing claim.&lt;/p&gt;

&lt;p&gt;They'll also catch me when I'm wrong. That's their job.&lt;/p&gt;

&lt;p&gt;Nothing gets past them.&lt;/p&gt;

&lt;p&gt;Candor Network is building safety infrastructure for platforms that can't afford to let things slip through. If you run a Mastodon instance, a Discourse forum, a Discord community, or any platform where harmful content is a real problem — Channel and Zoe are on duty. Reach us at &lt;a href="mailto:candortheopenfeednetwork@gmail.com"&gt;candortheopenfeednetwork@gmail.com&lt;/a&gt;. We answer everything ourselves.&lt;/p&gt;

&lt;p&gt;Follow Channel and Zoe on Mastodon: @&lt;a href="mailto:candornetwork@mastodon.social"&gt;candornetwork@mastodon.social&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>security</category>
      <category>api</category>
    </item>
    <item>
      <title>The Moderation System That Was Running Perfectly — On Nobody</title>
      <dc:creator>Ronny Cruz</dc:creator>
      <pubDate>Fri, 17 Jul 2026 17:27:37 +0000</pubDate>
      <link>https://dev.to/candornetwork/the-moderation-system-that-was-running-perfectly-on-nobody-akf</link>
      <guid>https://dev.to/candornetwork/the-moderation-system-that-was-running-perfectly-on-nobody-akf</guid>
      <description>&lt;p&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I'm a solo founder building a social platform where content screening is the whole point. Livestreams get transcribed in real time and fanned out to AI analyzers — extremism detection, crisis detection — and the broadcaster's identity rides along so the system knows whose stream it's screening and can act on it.&lt;/p&gt;

&lt;p&gt;Identity flows through a JWT. Client logs in, gets a token, token accompanies the broadcast session, screening pipeline attributes everything correctly. Standard stuff. It worked in every test I ran during development.&lt;/p&gt;

&lt;p&gt;Here's the thing about this bug: &lt;strong&gt;there was no symptom.&lt;/strong&gt; No crash, no error log, no user complaint. Streams worked. Screening worked — I could see the analyzers firing. Everything looked healthy. That's what makes this one worth writing up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hunt
&lt;/h2&gt;

&lt;p&gt;I run my project under a rule I call §0: &lt;em&gt;"designed," "coded," and "verified working end-to-end" are three different statements, and only probes against the running system count as evidence.&lt;/em&gt; It exists because I once caught my deployed system quietly diverging from its design, one easy shortcut at a time, and I decided never again.&lt;/p&gt;

&lt;p&gt;So during a verification pass on the broadcast path, I wasn't asking "does streaming work?" — it obviously did. I was asking a more §0 question: &lt;strong&gt;prove to me that a logged-in broadcaster's session carries their verified identity into the screening pipeline.&lt;/strong&gt; Show me the actual value, from the actual running system.&lt;/p&gt;

&lt;p&gt;The probe came back wrong. The session that should have been my authenticated test user was attributed to &lt;code&gt;anonymous&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One session, maybe a fluke. I logged in again, fresh. Anonymous. Every single post-login session was anonymous.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dig
&lt;/h2&gt;

&lt;p&gt;Tracing it backwards through the running system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The client wasn't sending the token.&lt;/strong&gt; The broadcast token request went out without an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header. The session token existed in the app — it just never got attached to this particular request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server didn't mind.&lt;/strong&gt; The token route did no server-side JWT verification of its own. No token? No problem. It fell through to a default:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;broadcaster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;verifiedUser&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;And there was a third layer:&lt;/strong&gt; even when auth &lt;em&gt;did&lt;/em&gt; work, the client wasn't hydrating the session from storage after a page reload — so any refreshed session lost its identity anyway and fell into the same anonymous path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three small, defensible-looking decisions. A missing header. A convenient fallback. A skipped hydration step. Each one written with good intentions, none of them logging anything, and together they meant: &lt;strong&gt;after login, every session silently fell back to anonymous.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why does that matter beyond bookkeeping? Because anonymous sessions took a different code path — one that bypassed identity-attributed screening entirely. The moderation stack was deployed, healthy, and fully operational. It just wasn't in the loop for the sessions that mattered. A safety system in the "on" position, wired to nothing.&lt;/p&gt;

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

&lt;p&gt;Three parts, matching the three failures:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Server-side JWT verification on the token route.&lt;/strong&gt; The server now verifies the token itself, cryptographically, before issuing anything. Client claims are not trusted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client sends &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;&lt;/strong&gt; on the token request. The obvious missing piece, made explicit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;localStorage hydration on load&lt;/strong&gt;, so a refreshed session recovers its verified identity instead of silently downgrading.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And then the part I care about most — the fix behind the fix:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The fallback died.&lt;/strong&gt; Not just this one — the &lt;em&gt;pattern&lt;/em&gt;. My platform now runs a fail-loud rule: no silent defaults on anything security- or safety-relevant, anywhere. A missing secret at boot is &lt;code&gt;process.exit(1)&lt;/code&gt;, not &lt;code&gt;|| 'change-me'&lt;/code&gt;. A verification failure is a refusal, not a downgrade to a friendlier identity. If the system can't establish who you are, it doesn't guess — it stops, loudly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verified after the fix the same way it was caught: probes against the live system, logged with dates. Authenticated session in, verified identity out, screening attributed correctly, on the running box — not in my head, not in the code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this bug taught me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Silent fallbacks are camouflage.&lt;/strong&gt; &lt;code&gt;|| 'anonymous'&lt;/code&gt; looks like defensive programming. It's actually a machine for converting failures into fake success. The system didn't degrade visibly — it degraded into something that &lt;em&gt;looked identical to working.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It works" and "I verified it works" are different sentences.&lt;/strong&gt; Every test I ran during development passed, because development tests exercise the paths you thought of. The bug lived in the gap between the design in my head and the system on the box, and only a probe of the running system — asking it to prove its own behavior — could see into that gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The worst bugs don't throw.&lt;/strong&gt; Sentry-style error monitoring is essential, but this class of bug never generates an error to monitor — every code path "succeeded." The only detection mechanism is refusing to let failure be expressible as success: fail loud, fail closed on the paths that matter, and make silence itself impossible.&lt;/p&gt;

&lt;p&gt;This bug ultimately changed my platform's architecture — I rebuilt the write path so that this entire &lt;em&gt;class&lt;/em&gt; of bypass can't exist, which I wrote up separately in &lt;a href="https://dev.to/candornetwork/how-we-made-moderation-architecturally-impossible-to-skip-7lo"&gt;How We Made Moderation Architecturally Impossible to Skip&lt;/a&gt;. But the architecture came second. First came the humbling: my safety system spent its early life running perfectly, on nobody, and nothing anywhere was ever going to tell me.&lt;/p&gt;

&lt;p&gt;Now something does. Loudly.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
  </channel>
</rss>
