Grafana Loki Has a Free Log Aggregation System Like Prometheus But for Logs

Grafana Loki is a horizontally-scalable log aggregation system. Unlike Elasticsearch, it indexes only metadata (labels), not the full text.

What You Get for Free

  • No full-text indexing — stores compressed logs cheaply
  • Label-based querying with LogQL
  • Native Grafana integration
  • Multi-tenancy — built-in tenant isolation
  • S3/GCS backend — cheap object storage

Query Logs with LogQL

{job="nginx"} |= "error" | json | status >= 500

Why Loki Over Elasticsearch?

Feature Loki Elasticsearch
Storage cost 10-50x cheaper Expensive
Setup Minutes Hours
Best for Logs + Grafana Full-text search

Need observability infrastructure? Check my work on GitHub or email [email protected] for consulting.

Domina la Conectividad Privada en AWS con VPC Endpoints (¡Ahorra $/mes!)

Bienvenidos a una nueva entrega en el maravilloso mundo de las redes en AWS.

Muchos arquitectos caen en la trampa de desplegar un NAT Gateway solo para que sus servidores privados se comuniquen con servicios como Amazon S3 y DynamoDB. Esto no solo infla la factura, sino que puede resultar contraproducente si nuestro objetivo es mantener los recursos 100% aislados del mundo exterior. Por eso, hoy vamos a construir una arquitectura de conectividad privada, segura y, lo mejor de todo, con un costo de red de $0.

Es posible tener lo mejor de ambos mundos: seguridad extrema y ahorro total?

Arquitectura base

Para que este workshop sea fluido y nos enfoquemos en lo que realmente importa asegurar la conectividad privada entre nuestros servidores y servicios como S3 y DynamoDB he diseñado una plantilla de CloudFormation. Con ella, podrás desplegar en minutos la infraestructura base que incluye:

  1. VPC: Una red virtual aislada con una subred 100% privada (sin Internet Gateway).
  2. Tabla de Rutas: Asociada específicamente a nuestra subred privada.
  3. Instancia EC2: Un servidor sin dirección IP pública (totalmente invisible desde internet).
  4. Recursos de Datos: Un Bucket de Amazon S3 y una Tabla de Amazon DynamoDB listos para ser consumidos.
  5. Seguridad IAM: Un rol con el principio de mínimos privilegios para interactuar con S3 y DynamoDB de forma segura.
  6. Security Group: Configurado específicamente para nuestra instancia EC2.

💡 Nota de Arquitecto: Al no tener un Internet Gateway, nuestra instancia nace “ciega y sorda” hacia el exterior. Es el escenario perfecto para probar cómo los VPC Endpoints actúan como túneles privados dentro de la red global de AWS.

💡 Nota: Si te apasionan las redes y prefieres construir cada componente desde cero, en mi perfil de dev.to tengo exactamente lo que necesitas. He preparado dos workshops previos donde te explico paso a paso cómo crear una VPC, subredes, tablas de rutas y mucho más:

Si ya lo tienes controlado, crea el stack con la plantilla que preparé para ti y te dejo a continuación.

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Workshop Base: Red Privada Aislada para Práctica de VPC Endpoints'

Parameters:
  VpcCIDR:
    Type: String
    Default: 10.0.0.0/24
    Description: CIDR para la VPC del Workshop

  PrivateSubnetCIDR:
    Type: String
    Default: 10.0.0.0/25
    Description: CIDR para la Subred Privada (128 IPs)

  LatestImageId:
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64'


Resources:
  # --- INFRAESTRUCTURA DE RED ---
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 
        Ref: VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: Workshop-VPC-Endpoint

  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: 
        Ref: VPC
      CidrBlock: 
        Ref: PrivateSubnetCIDR
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false # Crucial: Asegura que ningun recurso se lance con ip publica
      Tags:
        - Key: Name
          Value: Subred-Aislada

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPC
      Tags:
        - Key: Name
          Value: Tabla-Ruteo-Privada

  SubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: 
        Ref: PrivateSubnet
      RouteTableId:
        Ref: PrivateRouteTable

  # --- SEGURIDAD (IAM) ---
  EC2InstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: [ec2.amazonaws.com]
            Action: ['sts:AssumeRole']
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
        - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
      Policies:
        - PolicyName: EC2ConnectPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2-instance-connect:SendSSHPublicKey
                Resource:
                  Fn::Sub: "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
       - Ref: EC2InstanceRole

  # --- SEGURIDAD (Security Groups) ---
  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Permite SSH solo desde el CIDR de la VPC (temporalmente)
      VpcId:
        Ref: VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp:
            Ref: VpcCIDR # Permitimos traficos desde la red interna

  # --- CÓMPUTO ---
  WorkshopInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      IamInstanceProfile:
        Ref: EC2InstanceProfile
      ImageId:
        Ref: LatestImageId
      SubnetId:
        Ref: PrivateSubnet
      SecurityGroupIds:
        - Ref: InstanceSecurityGroup
      Tags:
        - Key: Name
          Value: EC2-Privada-Workshop

  # --- RECURSOS DE DATOS (Para las pruebas) ---
  WorkshopBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: 
        Fn::Sub: 'workshop-s3-endpoint-${AWS::AccountId}-${AWS::Region}'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  WorkshopTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Delete
    Properties:
      TableName: Workshop-Dynamo-Endpoint
      AttributeDefinitions:
        - AttributeName: ID
          AttributeType: S
      KeySchema:
        - AttributeName: ID
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

Outputs:
  VpcId:
    Value:
      Ref: VPC
  SubnetId:
    Value:
      Ref: PrivateSubnet
  RouteTableId:
    Value:
      Ref: PrivateRouteTable
  BucketName:
    Value:
      Ref: WorkshopBucket

🚀 Despliegue de la infraestructura

Esta plantilla está diseñada para que la puedas desplegar en cualquier región. Yo personalmente estaré trabajando en Ohio (us-east-2); ya saben, para no estar tan “amorochados” en Virginia con el resto del mundo 😉.

Para lanzar el laboratorio, sigue estos pasos:

  1. Copia el código YAML de arriba y guárdalo en tu editor favorito con el nombre base-stack.yaml.

  2. Ve a la consola de AWS CloudFormation y selecciona Create stack > With new resources (standard).

  3. En la sección de Template source, elige Upload a template file y sube tu archivo.

  4. Asigna un nombre al stack (ej. Workshop-VPC-Endpoints) y deja los parámetros por defecto.

  5. Haz clic en Next hasta llegar al final, marca la casilla de “I acknowledge that AWS CloudFormation might create IAM resources” y presiona Submit.

Al finalizar la creación (después de unos 2 o 3 minutos), deberías ver el estado como: CREATE_COMPLETE.

Plan de batalla 🎯.

Para conquistar este laboratorio, nos dividiremos en tres objetivos estratégicos:

  • Misión 1: El Rescate (EIC Endpoint). Aprenderemos a entrar a nuestra instancia privada mediante EC2 Instance Connect (EIC) Endpoint. Olvídate de gestionar llaves .pem perdidas, de configurar un Bastion Host o de exponer tu servidor con una IP pública.

  • Misión 2: El Túnel de Objetos (S3 Gateway Endpoint). Configuraremos un acceso directo hacia Amazon S3. Veremos cómo podemos acceder a nuestros objetos sin que un solo paquete de datos toque la internet publica.

  • Misión 3: El Almacén de Datos (DynamoDB Gateway Endpoint). Estableceremos una conexión privada hacia nuestra base de datos NoSQL para asegurar que el tráfico de nuestra aplicación nunca salga de la red de AWS.

🛡️ Nota de Arquitecto: Aunque ambos son “Endpoints”, en las misiones 2 y 3 usaremos el tipo Gateway, que es un recurso gratuito y funciona mediante rutas en nuestra VPC, a diferencia del tipo Interface que usa direcciones IP privadas (ENI).

Misión 1: Rescate de la Instancia Privada (EIC Endpoint)

Si nos dirigimos a la consola de Amazon EC2, seleccionamos la instancia que desplegamos con nuestra plantilla de CloudFormation e intentamos conectarnos a ella utilizando el botón “Connect”, nos toparemos con un muro de advertencias:

“No public IPv4 or IPv6 address assigned.”

“Instance is not in a public subnet.”

AWS nos está confirmando que nuestra arquitectura de aislamiento funciona: la instancia no tiene “puertas” hacia el exterior. Tradicionalmente, aquí es donde muchos se rinden y crean un Bastion Host (un servidor extra que hay que pagar y parchar).

En resumidas cuentas, en este momento no podemos conectarnos a nuestra instancia. Pero no te preocupes, que para esto (y casi todo lo demás) existe… no, no es MasterCard 😉, es EC2 Instance Connect (EIC) Endpoint. Afortunadamente, contamos con esta funcionalidad que nos permite conectarnos a nuestros servidores en subredes privadas, incluso cuando están totalmente aislados del exterior.

Para configurarlo, sigamos estos pasos:

  1. Dirígete al servicio de VPC en la consola de AWS.

  2. En la columna de la izquierda, busca la sección PrivateLink and Lattice.

  3. Selecciona Endpoints y luego haz clic en el botón Create endpoint.

🛠️ Configuración del Endpoint: El “Apretón de Manos”

Al crear el endpoint, nos encontraremos con varios campos. Vamos a desglosarlos para que no haya pérdida:

  • Name: EIC-Connect (o el que prefieras).

  • Type, Category: Selecciona EC2 Instance Connect Endpoint.

  • VPC: Elige nuestra VPC del workshop: Workshop-VPC-Endpoint.

⚠️ Un pequeño paréntesis: El Security Group del Endpoint
Antes de terminar, necesitamos un Security Group (SG) específico para nuestro EIC. Este actuará como el “permiso de salida” para que el túnel llegue a la instancia.

Crea un nuevo SG con estos datos:

  • Name: SG-EIC-Endpoint

  • VPC: Workshop-VPC-Endpoint

  • Outbound Rule (Regla de Salida): Es la única que necesitamos.

  • Protocolo: TCP

  • Puerto: 22 (SSH)

  • Destino: 10.0.0.0/25 (El CIDR de nuestra subred privada).

💡 Tip de Arquitecto: Usamos el CIDR 10.0.0.0/25 para que este endpoint nos sirva para cualquier instancia que lancemos en nuestra subred privada. Si quisiéramos ser ultra-estrictos (Zero Trust), en el Destino podríamos poner directamente el ID del Security Group de la instancia.

Listo! Con el Security Group creado, regresemos a la pestaña de creación del Endpoint:

  • En el apartado de Security groups, haz clic en el ícono de refrescar para que aparezca nuestro recién creado SG-EIC y selecciónalo.

  • En la sección de Subnet, selecciona la subred privada de nuestra VPC, asegúrate de que esté marcada la opción IPv4 y haz clic en Create endpoint.

La creación del recurso puede tomar un par de minutos. Mientras esperas, puedes tomar un café ☕. En cuanto veas el estado (Status) como Available, significa que nuestro túnel privado está listo para operar y conectarnos a la instancia.

A continuación, te dejo una imagen de referencia con la configuración completa del EIC Endpoint:

Misión 1: ¡Conexión Exitosa! 🚀

¡Llegó el momento de la verdad! Vamos a entrar a nuestra instancia privada sin necesidad de una IP pública:

  1. Regresa al servicio de Amazon EC2 y selecciona tu instancia.

  2. Haz clic en el botón Connect.

  3. En las pestañas superiores, selecciona EC2 Instance Connect.

  4. En la sección de Connection Type, elige la opción: Connect using a Private Ip (EC2 Instance Connect Endpoint).

  5. En el menú desplegable de EC2 Instance Connect Endpoint, selecciona el Endpoint que creamos (EIC-Connect).

  6. Finalmente, haz clic en Connect.

En este punto, ya deberíamos estar dentro de nuestro servidor EC2 sintiéndonos como auténticos hackers 💻. Ver ese prompt de la terminal después de haber pasado por una red 100% privada es la prueba de que nuestro túnel seguro funciona a la perfección.

Hasta este punto, no tenemos ninguna comunicación establecida con Amazon S3 ni con DynamoDB. Como nuestra instancia está en una subred privada sin salida a internet, ¿qué crees que pasaría si intentamos listar nuestros recursos desde la AWS CLI?

¡Hagamos la prueba de fuego! Ejecuta estos comandos en tu terminal:

# Intento de listar buckets
aws s3 ls

# Intento de listar tablas, pásale al comando la región en la que estas trabajando.
aws dynamodb list-tables --region us-east-2

¿Qué sucede? Exacto: Silencio total. La terminal se quedará “congelada” intentando alcanzar los endpoints públicos de AWS hasta que finalmente recibas un Connection Timeout.

💡 Explicación: Aunque los servicios de AWS son internos a la red de Amazon, sus APIs (los puntos a los que llamamos por CLI) residen en el internet público. Como nuestra instancia es “tímida” y no tiene salida al exterior, no sabe cómo llegar a ellos. De hecho, te invito a que revises la tabla de rutas “Tabla-Ruteo-Privada” creada por nuestro stack: notarás que no existe ninguna ruta (ni un Internet Gateway ni un NAT Gateway) que le indique al tráfico hacia dónde ir. Para nuestra instancia, el resto del mundo no existe… todavía.

Así que, para lograr nuestro objetivo de conectividad total sin comprometer la seguridad, vamos a implementar los Gateway Endpoints tanto para S3 como para DynamoDB.

Misión 2: Misión 2: El Túnel Privado para Amazon S3

Un Gateway Endpoint es, en esencia, un puente privado que creamos dentro de nuestra VPC para comunicarnos con los servicios de AWS sin que los datos salgan jamás de la red interna de Amazon. Es decir: logramos un contacto cero con el internet público.

A diferencia de otros tipos de endpoints, el de tipo Gateway no utiliza direcciones IP privadas en tu subred; en su lugar, funciona mediante “Prefix Lists” (listas de prefijos) que se añaden automáticamente a tu Tabla de Rutas.

💰 Dato para tu bolsillo: Los Gateway Endpoints para S3 y DynamoDB son gratuitos. No hay cargos por hora.

  1. Regresa al servicio de VPC y, en la columna izquierda, selecciona Endpoints seguido del botón “Create endpoint”.
  2. En Service category, asegúrate de que esté seleccionado AWS services.
  3. En la barra de búsqueda de Services, escribe: Gateway.
  4. ¡Punto Crucial! Busca el servicio que termina en .s3 y asegúrate de que el Type sea Gateway..

Nota: También verás uno de tipo “Interface”, pero esa es harina de otro costal (y tiene costo). Quédate con el Gateway, que es el héroe gratuito de esta historia.

  • En el desplegable de VPC, selecciona nuestra: Workshop-VPC-Endpoint.

  • Configuración de Red: Aquí debes marcar el check de nuestra Tabla-Ruteo-Privada.

¿Por qué? Al hacer esto, CloudFormation (o la consola) agregará automáticamente una ruta “mágica” que dice: “Todo el tráfico que vaya hacia S3, envíalo por este túnel privado”. Sin esto, tu instancia seguirá gritando al vacío.

  • Para finalizar, haz clic en Create endpoint.

Esto puede tardar un par de minutos en aparecer como Available. Mientras tanto, ¡aprovechemos el tiempo!

Podemos adelantar la Misión 3: El Almacén de Datos (DynamoDB Gateway Endpoint) siguiendo exactamente los mismos pasos que acabamos de realizar. La única diferencia es que, en la barra de búsqueda de servicios, esta vez seleccionarás el que termina en .dynamodb (asegurándote de que el tipo sea Gateway).

No olvides asociarlo también a nuestra Tabla-Ruteo-Privada para que la magia de las rutas se aplique a ambos servicios.

Con nuestros Gateway Endpoints en estado Available, ha llegado el momento de volver a la terminal y reclamar nuestra victoria. Vamos a intentar listar de nuevo nuestro bucket de S3 y nuestra tabla de DynamoDB para confirmar que la comunicación fluye por el túnel privado:

🔍 Ahora puedes volver a nuestra Tabla-Ruteo-Privada y notarás que la magia ha sucedido: han aparecido dos nuevas rutas con identificadores que empiezan por pl-xxxxxxxx (Prefix Lists). Estas entradas apuntan directamente hacia S3 y DynamoDB.

Esas rutas son las que le indican a tu subred: “Si alguien pregunta por estos servicios, no busques en internet; envíalo por el túnel privado”. ¡El camino ya está despejado!

# Listar Buckets de S3
aws s3 ls

# Listar Tablas de DynamoDB, no olvides colocar tu región
aws dynamodb list-tables --region us-east-2

¡Yeahhh! 🚀 Si ves el nombre de tu bucket y de tu tabla en pantalla, ¡felicidades! Acabas de establecer una conexión privada, segura y, lo más importante, totalmente gratuita.

🧠 ¿Qué pasó tras bastidores? Cuando ejecutaste los comandos, tu VPC consultó la Tabla de Rutas, vio que el destino era S3/DynamoDB y, en lugar de intentar salir a internet, envió el tráfico directamente a través del Endpoint correspondiente para cada servicio. ¡Magia de redes en su máxima expresión!

¡Todo ha salido a la perfección! Ahora contamos con una arquitectura robusta y segura donde una instancia EC2, totalmente aislada en una subred privada y sin IP pública, puede comunicarse fluidamente con nuestros servicios de Amazon S3 y DynamoDB.

Partiendo de aquí, el cielo es el límite. Ustedes ya pueden realizar cualquier operación (subir archivos, consultar tablas, etc.) desde su instancia, siempre y cuando adjunten las políticas de IAM correctas al Role o Instance Profile que definimos en nuestro Stack inicial.

🧹 Limpieza: ¡No dejes la luz encendida!

Antes de irnos a celebrar nuestro éxito, es fundamental limpiar los recursos para mantener nuestra cuenta en $0 USD. Como mezclamos automatización con pasos manuales, el orden de eliminación es vital:

  1. Elimina los Endpoints manualmente: Ve a la sección de Endpoints en la consola de VPC, selecciona los tres que creamos (S3, DynamoDB y EIC) y elígelos para eliminarlos. CloudFormation no puede borrar lo que él no creó.

  2. Elimina el Security Group del EIC: Una vez borrado el endpoint, ve a Security Groups y elimina el SG-EIC-Endpoint.

  3. Elimina el Stack de CloudFormation: Ahora sí, ve a la consola de CloudFormation, selecciona tu stack y dale a Delete. Esto borrará la EC2, la VPC y los roles automáticamente.

🏁 Conclusión

Dominar la conectividad privada es lo que separa a un administrador de nubes de un verdadero Arquitecto Cloud. Hoy demostramos que es posible construir una fortaleza segura, aislada y con un costo de red de $0, sin necesidad de NAT Gateways costosos para tareas simples.

Espero que este workshop te sirva para blindar tus arquitecturas y optimizar el presupuesto de tus proyectos.

¿Te quedó alguna duda sobre los tipos de Endpoints o la configuración de rutas? ¡Hablemos en los comentarios! 💬

Beyond the Panic: Hardening the Rust SDK

TL;DR

Engineering is often the art of managing the “unhappy path.” This week was a testament to that philosophy. With 74 commits, 2 PRs, 3 issues, and 3 reviews across 5 repositories, the focus shifted from building new features to fortifying the foundations. The headline? A concerted effort to purge unwrap() and panic calls from our Rust SDKs in favor of robust, Result-based error handling.

The Work: Velocity in the Engine Room

The bulk of the week’s momentum was concentrated in the Python ecosystem, specifically within p2pCalc and AgentPay. These two repositories accounted for 54 of the week’s 74 commits, representing a high-velocity push in our core logic layers.

p2pCalc & AgentPay

In p2pCalc (32 commits), we focused on the computational integrity of our peer-to-peer logic. While the additions and deletions were kept internal to the commit history, the sheer volume of activity suggests a deep dive into the algorithmic core. Parallel to this, AgentPay (22 commits) saw significant movement. When dealing with payment agents, there is no room for ambiguity. My focus here was on ensuring that the state machine governing agent transactions is both predictable and idempotent.

DevNotion & The Developer Environment

Meanwhile, DevNotion (14 commits) continues to evolve. As a TypeScript-based project, it serves as the bridge between our raw data and the narratives we share. The work here was iterative, likely refining the very engine that powers these weekly field notes.

I also took a moment to sharpen the saw. Five commits to my nvim configuration (Lua) might seem like minor housekeeping, but for a principal engineer, the editor is the primary instrument. A well-tuned environment isn’t a luxury; it’s a prerequisite for the kind of deep work required to handle complex refactors.

The Pull Requests: The Safety Initiative

The most critical architectural work happened within the Appwrite ecosystem. We are currently in the process of maturing the Rust SDK, and that means moving away from “prototype-grade” code.

Title Repo State Impact
fix(rust): replace unwrap/panic with Result-based error handling sdk-generator OPEN Prevents generator-level crashes during SDK synthesis.
refactor: replace unwrap with error handling in Client methods sdk-for-rust CLOSED Shifts error responsibility to the library consumer, avoiding runtime panics.

The PR in sdk-for-rust (#9) was a substantial undertaking, involving 149 additions and 46 deletions. In Rust, using .unwrap() is a debt you eventually have to pay. By replacing these with proper Result types, we ensure that the SDK doesn’t bring down a user’s entire application just because of a transient network failure or a malformed response. It’s about respecting the caller’s right to decide how to handle failure.

The work continues in sdk-generator (#1398), where I am applying similar logic to the generator itself. If the generator panics, the entire CI/CD pipeline halts. By implementing Result-based handling, we allow the system to fail gracefully and provide actionable feedback.

The Conversations: Setting the Standard

Collaboration this week was centered on documentation and issue tracking. I closed two critical issues in the sdk-for-rust repo:

  1. Issue #8: The bug report that initiated the unwrap() refactor.
  2. Issue #7: The creation of CONTRIBUTING.md.

The latter is particularly important. As we scale, we need to ensure that external contributors understand the quality bar we’ve set. A clear CONTRIBUTING.md is the first step in building a healthy open-source community around the Rust SDK.

The Reviews: Raising the Bar

Reviewing 3 PRs while authoring 2 is a healthy ratio for a week focused on quality. My reviews on awesome-py-libp2p-examples resulted in “CHANGES_REQUESTED” for two separate PRs (#6 and #3).

When reviewing collaborative tools like a “Whiteboard” or “p2p model checking,” I look for more than just “does it run?” I look for idiomatic patterns and clear documentation. If an example is meant to be “awesome,” it must serve as a gold standard for others to follow. My comments on py-libp2p (#1222) regarding the reorganization of example scripts follow this same theme: structure dictates usability.

The Craft: A Polyglot Reality

This week’s data reveals a truly polyglot workflow. While Python remains the dominant language by volume (over 77MB of context), the most impactful changes were written in Rust and TypeScript.

The add/delete ratio (+227/-80) indicates a week of net-new growth, but the nature of that growth was defensive. We weren’t just adding features; we were adding safety nets. The lack of a commit streak this week is a reminder that engineering isn’t always a daily sprint; sometimes it’s a series of deep-dive sessions separated by the necessary time to think through architectural implications.

The Horizon

As we look toward next week, the primary objective is to land the open PR in the sdk-generator. Once the generator is hardened, we can begin rolling out these safety improvements across all generated Rust SDKs.

We’ve moved the needle on reliability this week. The transition from “it works” to “it’s resilient” is often invisible to the end-user until something goes wrong and the system doesn’t crash. That is the quiet victory of the seasoned architect.

Onward.

Building an AI Profanity Filter with Vocal Separation

I built an online tool that automatically detects and bleeps profanity in video and audio files. Here’s the high-level architecture.

The problem

Manual profanity censoring takes 45+ minutes for a 10-minute video. You have to listen through, find each word, razor the audio, drop a beep effect. For songs, it’s nearly impossible without destroying the music.

The solution

AI speech recognition + neural vocal separation.

How it works

  1. User uploads a file or pastes a YouTube URL
  2. Audio is extracted with FFmpeg
  3. AI speech-to-text transcribes the audio (AssemblyAI / Deepgram)
  4. Profanity is detected using morphological analysis (lemmatization)
  5. Each word is replaced with beep/silence/custom sound via FFmpeg
  6. For songs: Demucs AI separates vocals from instruments first

Song mode — the hard part

Demucs by Meta AI does the heavy lifting — splitting audio into vocal and instrumental tracks. Profanity detection runs only on the vocal track, then the censored vocals are mixed back with the original instruments. The music stays untouched.

Stack

  • Frontend: Next.js (React)
  • Backend: NestJS (Node.js), BullMQ queues
  • Audio processing: Python (FastAPI), Demucs, FFmpeg
  • Infrastructure: Docker Compose, PostgreSQL, Redis

Results

12,000+ files processed. Three processing modes: standard (clean speech), precise (noisy audio), enhanced (songs with vocal separation).

Free for up to 15 minutes per month at videocensor.net.

Would love to hear your thoughts!

How to Build an Automated TikTok Pipeline from UGC Clips

TL;DR

Built a 3-step pipeline for automated TikTok posting from UGC clips: scrape-hooks.js (collection) → trim-and-stitch.js (editing) → post-to-postiz.js (publishing). Achieved 100% success rate on day one with 4 daily runs (8AM/5PM, JP/EN). Clear separation of concerns enables easy debugging and component swapping.

Source: daily.dev: How to write viral stories for developers
Key quote: “Write from expertise. Developers hate clickbait.”

Prerequisites

  • Node.js v18+
  • Postiz API account (TikTok integration enabled)
  • UGC clip storage (workspace/hooks/ugc-clips/)
  • ffmpeg (for video trimming)

The Problem: Why Existing Solutions Failed

Our existing posting skills had limitations:

Approach Limitation
Larry slideshow Static images only. Can’t use video clips
ReelClaw Posts single videos as-is. No multi-clip editing

What we needed: Multiple UGC clips → auto-trim → stitch into one video → post to TikTok

Step 1: Pipeline Design (3-Step Separation of Concerns)

Source: Unix Philosophy
Key quote: “Write programs that do one thing and do it well. Write programs to work together.”

Step Script Role Input Output
1 scrape-hooks.js Collect & select UGC clips workspace/hooks/ugc-clips/ workspace/hooks/slot-08-00-ja.json
2 trim-and-stitch.js Trim & stitch videos slot-08-00-ja.json workspace/output/final-08-00-ja.mp4
3 post-to-postiz.js Post via Postiz API final-08-00-ja.mp4 TikTok post published

Why 3 separate scripts:

  • Single responsibility → easier debugging
  • Loose coupling via JSON → swap components freely
  • ffmpeg/Postiz failures don’t cascade

Step 2: scrape-hooks.js (Collection)

// Read candidate clips from workspace/hooks/ugc-clips/
const clipPool = fs.readdirSync('/Users/anicca/.openclaw/workspace/hooks/ugc-clips')
  .filter(f => f.endsWith('.mp4'));

// Select unused clips randomly
const selectedClips = clipPool
  .filter(clip => !usedClips.includes(clip))
  .sort(() => Math.random() - 0.5)
  .slice(0, 3); // Select 3 clips

// Save to slot JSON
const slotData = {
  clips: selectedClips.map(name => ({
    path: `/Users/anicca/.openclaw/workspace/hooks/ugc-clips/${name}`,
    duration: 10 // seconds (use ffprobe for accuracy)
  })),
  caption: generateCaption(), // Hook generation (separate function)
  hashtags: ['#selfcare', '#mindfulness', '#healing']
};
fs.writeFileSync(`workspace/hooks/slot-08-00-ja.json`, JSON.stringify(slotData, null, 2));

Key points:

  • Track used clips (used-clips.json) to avoid duplicates
  • Random shuffle for variety
  • Fixed 3 clips (fits TikTok 15-60s recommendation)

Step 3: trim-and-stitch.js (Editing)

const ffmpeg = require('fluent-ffmpeg');
const slotData = JSON.parse(fs.readFileSync('workspace/hooks/slot-08-00-ja.json'));

// Trim each clip to 10 seconds
const trimmedPaths = [];
for (const [i, clip] of slotData.clips.entries()) {
  const outputPath = `/tmp/trimmed-${i}.mp4`;
  await new Promise((resolve, reject) => {
    ffmpeg(clip.path)
      .setStartTime(0)
      .setDuration(10)
      .output(outputPath)
      .on('end', resolve)
      .on('error', reject)
      .run();
  });
  trimmedPaths.push(outputPath);
}

// Concatenate 3 clips into 1
const finalPath = 'workspace/output/final-08-00-ja.mp4';
await new Promise((resolve, reject) => {
  const cmd = ffmpeg();
  trimmedPaths.forEach(path => cmd.input(path));
  cmd
    .complexFilter('[0:v][1:v][2:v]concat=n=3:v=1:a=0[outv]', ['outv'])
    .outputOptions('-map', '[outv]')
    .output(finalPath)
    .on('end', resolve)
    .on('error', reject)
    .run();
});

console.log(`Final video: ${finalPath}`);

Key points:

  • fluent-ffmpeg with Promise wrappers for error handling
  • Intermediate files in /tmp → only final output in workspace
  • concat filter with no audio (TikTok allows separate BGM)

Step 4: post-to-postiz.js (Publishing)

const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');

const slotData = JSON.parse(fs.readFileSync('workspace/hooks/slot-08-00-ja.json'));
const videoPath = 'workspace/output/final-08-00-ja.mp4';

// 1. Upload video (Postiz Media API)
const form = new FormData();
form.append('file', fs.createReadStream(videoPath));
const uploadRes = await axios.post('https://api.postiz.com/public/v1/media/upload', form, {
  headers: { 
    ...form.getHeaders(),
    'Authorization': process.env.POSTIZ_API_KEY 
  }
});
const mediaId = uploadRes.data.id;

// 2. Create post (Postiz Posts API)
await axios.post('https://api.postiz.com/public/v1/posts', {
  integrationId: process.env.POSTIZ_TIKTOK_JP_INTEGRATION_ID, // TikTok JP
  content: `${slotData.caption}nn${slotData.hashtags.join(' ')}`,
  mediaIds: [mediaId],
  scheduleAt: new Date().toISOString() // Immediate posting
}, {
  headers: { 'Authorization': process.env.POSTIZ_API_KEY }
});

console.log('Posted to TikTok via Postiz');

Key points:

  • Postiz API requires 2 steps (media upload → post creation)
  • integrationId specifies account (JP/EN separate)
  • scheduleAt for immediate or scheduled posting

Source: Postiz API Documentation
Key quote: “Upload media first using /media/upload, then reference mediaIds in /posts”

Step 5: Cron Configuration (4 Daily Runs)

# ~/.openclaw/workspace/cron-jobs.json (OpenClaw Gateway)
{
  "name": "mau-tiktok-ja-morning",
  "schedule": { "kind": "cron", "expr": "0 8 * * *", "tz": "Asia/Tokyo" },
  "payload": {
    "kind": "agentTurn",
    "message": "Execute mau-tiktok skill for JA morning slot (08:00)"
  },
  "sessionTarget": "isolated"
}

4 cron jobs:

  • mau-tiktok-ja-morning (08:00 JST)
  • mau-tiktok-en-morning (08:15 JST)
  • mau-tiktok-ja-evening (17:00 JST)
  • mau-tiktok-en-evening (17:15 JST)

Why 15-minute intervals:

  • Avoid Postiz API rate limits
  • Prevent parallel ffmpeg processes (CPU spike prevention)

Production Results (2026-03-27)

Slot Time Result Duration
ja-morning 08:00 ✅ ok 2m 15s
en-morning 08:15 ✅ ok 2m 08s
ja-evening 17:00 ✅ ok 2m 12s
en-evening 17:15 ✅ ok 2m 20s

Success rate: 4/4 = 100% (day one)

Troubleshooting (Issues Encountered in Production)

Issue Cause Solution
ffmpeg concat error Resolution/FPS mismatch Pre-normalize all clips to 1080×1920 30fps
Postiz 413 Payload Too Large Video size >100MB Add -crf 23 compression during trim
Black screen on TikTok Unsupported codec Specify -c:v libx264 -pix_fmt yuv420p

Key Takeaways

Lesson Detail
3-step separation Collection, editing, publishing as independent scripts → easy debugging, swappable components
Loose coupling via JSON Filesystem-based state between steps → stateless, re-runnable
ffmpeg error handling Promise-wrapped fluent-ffmpeg + try-catch → cleanup intermediate files on failure
Postiz 2-step API Media upload → post creation order → avoid 403/422 errors
15-min cron intervals Distribute rate limits & CPU load → stable operation
Day-one 100% success Clear design + API reuse → minimize risk for new skills

Next steps:

  • Auto-replenish clip pool (scrape YouTube Shorts/Instagram Reels)
  • LLM-powered caption generation (auto-generate hooks)
  • Engagement tracking (Postiz Analytics API → prioritize high-performing clips)

Source: Copyblogger: 22 Best Headline Formulas
Key quote: “8 out of 10 people will read the headline. Only 2 will read the rest.”

(This article is based on production results. Code is simplified but structurally identical to implementation.)

Digital Debt Tracker

I built a Digital Debt Tracker to stop losing money
on forgotten subscriptions 💳

We all have subscriptions we forgot about — Netflix,
Spotify, random trials that auto-renewed. I built this
app to track them all in one place.

What it does

  • Dashboard to see all subscriptions at a glance
  • Monthly & yearly spending totals
  • Track billing dates & trial expirations
  • Add notes and categories for each subscription

Tech Stack

  • Frontend: React 18
  • Backend: Node.js + Express
  • Database: SQLite
  • Auth: JWT authentication

Why I built it

Most people lose $50-200/year on forgotten
subscriptions. This app solves that.

GitHub

https://github.com/GGWarrior001/Digital-Debt-Tracker

Drop a ⭐ if you find it useful!

Building a Chat Assistant Module for Magento 2: Observers, Message Queues, and 10K Products

Magento stores are large. Not WooCommerce “500 products with a few categories” large. Magento stores run 10,000+ SKUs, configurable products with dozens of variations, multiple store views for different languages, and Multi-Source Inventory across warehouses.

I built Emporiqa, a chat assistant for e-commerce stores. After shipping integrations for Drupal Commerce, WooCommerce, and Sylius, Magento was next. The catalog complexity made it the most interesting one to build.

Here’s how the module works under the hood.

The Problem: Chat That Knows Nothing

Most chat solutions for Magento are JavaScript widgets that sit on the page and know nothing about your catalog. Customer asks “do you have running shoes under 80 euros in size 42?” The widget either sends them to the search page or gives a generic response.

To answer product questions, the chat assistant needs the actual catalog data: product names, descriptions, prices, stock levels, categories, images, and variation attributes. For Magento, it also needs to understand configurable product relationships and handle per-store-view translations.

Architecture: Observers → Queue → Webhooks

The module doesn’t run language models inside Magento. That would tank admin performance. Instead, it sends product and page data to Emporiqa via webhooks, and the chat processing happens on separate infrastructure.

The flow:

  1. Magento observer fires on catalog_product_save_after, catalog_product_delete_before, cms_page_save_after, etc.
  2. The observer formats the product into a consolidated payload (all store view translations in one event)
  3. The event is published to Magento’s DB message queue (emporiqa.webhook.consumer)
  4. The queue consumer sends the webhook to Emporiqa with HMAC-SHA256 signature

The flow is non-blocking: the admin saves a product, the observer runs in milliseconds, and the actual HTTP request happens asynchronously via the queue consumer.

Configurable Products: Parent/Child Consolidation

This was the tricky part. Magento’s configurable products have a parent that holds the name, description, and images, plus child simple products that hold the actual SKU, price, and stock.

The module syncs both:

Parent (configurable):
  identification_number: "product-123"
  sku: "JACKET-BLUE"
  is_parent: true
  variation_attributes: {"base": {"en-us": ["Color", "Size"], "de-de": ["Farbe", "Größe"]}}
  names: {"base": {"en-us": "Trail Jacket", "de-de": "Wanderjacke"}}

Child (simple):
  identification_number: "variation-124"
  sku: "JACKET-BLUE-M"
  is_parent: false
  parent_sku: "JACKET-BLUE"
  prices: {"base": [{"currency": "EUR", "current_price": 79.99}]}
  stock_quantities: {"base": 42}

On the Emporiqa side, the assistant understands that “Trail Jacket” comes in multiple colors and sizes, with specific prices and stock per variation. A customer asking “do you have the trail jacket in medium?” gets an answer based on actual inventory.

Multi-Store-View Consolidation

Magento uses store views for languages. A product might have an English name in the default store view and a German name in the de_DE store view.

Rather than sending one webhook per store view (which would multiply requests by the number of languages), the module consolidates everything into one payload per product:

{
  "names": {
    "base": {
      "en-us": "Summer Jacket",
      "de-de": "Sommerjacke"
    }
  },
  "descriptions": {
    "base": {
      "en-us": "Lightweight jacket for warm days",
      "de-de": "Leichte Jacke für warme Tage"
    }
  }
}

The channel key "base" is the Magento website code. If you have multiple websites (e.g., base, b2b), products nest under each website’s code as the channel key.

The ProductFormatter service handles this. It iterates over all enabled store views, loads the product in each store view’s context, and merges the translations into one structure.

MSI: Salable Quantity vs Physical Stock

Magento’s Multi-Source Inventory (MSI) tracks stock across multiple warehouses. Customers care about the salable quantity (what’s actually available to sell after reservations), not the physical stock at any individual source.

The module uses MSI’s GetProductSalableQtyInterface when available and falls back to StockRegistryInterface for stores not using MSI:

// Simplified: the module checks for MSI availability at runtime
try {
    $salableQty = $this->getProductSalableQty->execute($sku, $stockId);
} catch (Exception $e) {
    // Fallback to legacy stock
    $stockItem = $this->stockRegistry->getStockItemBySku($sku);
    $salableQty = $stockItem->getQty();
}

This handles both Magento Open Source (which may not have MSI) and Adobe Commerce (which always does).

Message Queue: Why Not Direct HTTP

I could have sent webhooks directly from the observer. Drupal’s integration does something similar with queue workers. But Magento’s observer system runs inside the request lifecycle, and making HTTP calls during catalog_product_save_after would slow down admin operations.

Magento’s DB message queue solves this. The observer publishes a message, the consumer processes it later. For large catalog imports (think: importing 5,000 products via CSV), the queue absorbs the load and the consumer works through them at its own pace.

The consumer is either started manually (bin/magento queue:consumers:start emporiqa.webhook.consumer) or runs via Magento’s cron-based consumer processing.

Cart and Order Tracking

The module exposes REST endpoints at /rest/V1/emporiqa/cart/* for in-chat cart operations (add, remove, update, checkout URL). All use same-origin session cookies, no API tokens needed. The chat widget’s EmporiqaCartHandler JavaScript calls these automatically.

For order tracking, a separate endpoint at /rest/V1/emporiqa/order/tracking accepts HMAC-signed requests from Emporiqa. The customer must verify their billing email before any order data is returned. Requests older than 5 minutes are rejected.

CLI Commands for Automation

Sync commands for deployment pipelines or scheduled jobs:

bin/magento emporiqa:sync:products          # Full product sync
bin/magento emporiqa:sync:pages             # Full CMS page sync
bin/magento emporiqa:sync:all               # Both
bin/magento emporiqa:test-connection         # Verify webhook connectivity
bin/magento emporiqa:sync:products --dry-run # Test without sending
bin/magento emporiqa:sync:products --batch-size=25

Each sync creates a session (sync.start → batched events → sync.complete). Items not seen during the session are marked as deleted on the Emporiqa side, so you don’t need to manually delete stale products.

What Doesn’t Work Well

Grouped and bundle products sync as standalone items without parent/child relationships. The module handles simple and configurable products with full parent/child consolidation. Grouped and bundle product types sync their component products individually, without the grouping structure.

The queue consumer needs babysitting. If emporiqa.webhook.consumer dies (OOM, timeout, server restart), webhook events pile up silently. Magento’s cron-based consumer processing is the safer option for production, but it adds latency. There’s no built-in alerting when the queue backs up.

CMS page sync is basic. Magento’s CMS pages are flat content blocks. The module syncs title, content, and URL, but can’t handle Page Builder layouts or widget directives. Stores using complex Page Builder pages get the raw content, which isn’t always useful.

What I Learned

Three things stood out building this compared to the WooCommerce and Sylius integrations:

  1. MSI detection is messy. You can’t assume MSI is present. Magento Open Source installs may have it removed. The module needs graceful fallback at runtime, not compile time.

  2. Configurable product observers fire for children too. When saving a configurable product, Magento also saves each child. Without deduplication, you’d send the same parent product multiple times. The queue handles this with a simple check before publishing.

  3. Store view context switching is expensive. Loading a product in store view A, then store view B, then back to A involves Magento’s store emulation. The formatter batches all store views together to minimize context switches.

Try It

The module is on the Adobe Commerce Marketplace and supports Magento 2.4.4+ (both Open Source and Adobe Commerce).

composer require emporiqa/module-chat-assistant
bin/magento module:enable Emporiqa_ChatAssistant
bin/magento setup:upgrade
bin/magento cache:flush

Start with a free sandbox store (100 products and 20 pages, no credit card). Sync some of your catalog, test with questions your customers ask, and see if the results make sense.

Full setup guide: emporiqa.com/docs/magento/. I also wrote about the full Magento integration on the Emporiqa blog.

Cross-Chain Bridging for AI Agents: LI.FI and Across

Your trading bot needs to move funds from Ethereum to Solana, then swap for USDC, then stake on Jito — all programmatically. Instead of juggling multiple APIs, wallet connections, and bridge interfaces, what if you could execute cross-chain strategies with simple REST calls?

The Multi-Chain DeFi Problem

DeFi is fragmented across chains. Your yield strategy might require Aave lending on Ethereum, Jupiter swaps on Solana, and Drift perpetuals — but each protocol has different APIs, signing patterns, and wallet requirements. Building a cross-chain trading system means:

  • Managing wallets on multiple chains
  • Learning each protocol’s SDK (Jupiter, Across, LI.FI all have different interfaces)
  • Handling bridge timing and gas optimization
  • Coordinating transaction sequences across chains

The complexity compounds when you’re building AI agents that need to execute these strategies autonomously. Your agent shouldn’t need to understand Ethereum gas mechanics AND Solana transaction versions AND bridge settlement times.

Cross-Chain Bridges in WAIaaS

WAIaaS provides unified REST APIs for cross-chain bridging through LI.FI and Across protocols. Your AI agent gets one interface to move funds between any supported chains, with automatic route optimization and transaction monitoring.

LI.FI aggregates bridge liquidity across protocols like Hop, Connext, and Stargate. Across specializes in fast, capital-efficient bridging between major EVM chains. WAIaaS handles the complexity — your agent just specifies source chain, destination chain, and amount.

Bridge Integration Example

Here’s how to bridge USDC from Ethereum to Polygon using LI.FI:

curl -X POST http://127.0.0.1:3100/v1/actions/lifi/bridge 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{
    "fromChain": "ethereum-mainnet",
    "toChain": "polygon-mainnet",
    "fromToken": "0xA0b86a33E6441146C3039A5CB6EfF1f1De03aAef",
    "toToken": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
    "amount": "100000000",
    "recipient": "0x742d35Cc6B99D100c3b3AA5F9A05d6d4b2f5B8A4"
  }'

WAIaaS returns the transaction ID immediately. The bridge operation happens asynchronously — you can monitor progress through the transaction status API:

curl http://127.0.0.1:3100/v1/transactions/<tx-id> 
  -H "Authorization: Bearer wai_sess_<token>"

The response includes bridge-specific status like settlement time estimates and destination transaction hashes once available.

Cross-Chain Strategy Automation

With 14 DeFi protocol providers integrated (Aave, Jupiter, Lido, Drift, Hyperliquid, and more), you can build sophisticated cross-chain strategies. Here’s a yield optimization sequence:

  1. Bridge USDC from Ethereum to Solana via LI.FI
  2. Swap half to SOL using Jupiter
  3. Stake SOL with Jito liquid staking
  4. Use remaining USDC for Drift perpetual position

Each step is a single API call:

# Step 1: Bridge USDC ETH → SOL
curl -X POST http://127.0.0.1:3100/v1/actions/lifi/bridge 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{"fromChain": "ethereum-mainnet", "toChain": "solana-mainnet", ...}'

# Step 2: Swap USDC → SOL on Jupiter  
curl -X POST http://127.0.0.1:3100/v1/actions/jupiter-swap/swap 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{"inputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "outputMint": "So11111111111111111111111111111111111111112", ...}'

# Step 3: Stake SOL with Jito
curl -X POST http://127.0.0.1:3100/v1/actions/jito-staking/stake 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{"amount": "5000000000"}'

# Step 4: Open Drift perp position
curl -X POST http://127.0.0.1:3100/v1/actions/drift/place-order 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{"marketIndex": 0, "direction": "long", "baseAssetAmount": "1000000000"}'

Policy-Controlled Bridge Operations

Cross-chain bridges involve significant funds movement. WAIaaS policy engine provides granular controls for bridge operations:

# Create bridge-specific policy
curl -X POST http://127.0.0.1:3100/v1/policies 
  -H "X-Master-Password: <password>" 
  -d '{
    "type": "ALLOWED_NETWORKS",
    "rules": {
      "networks": [
        {"network": "ethereum-mainnet"},
        {"network": "polygon-mainnet"},
        {"network": "solana-mainnet"}
      ]
    }
  }'

This policy blocks bridge operations to unauthorized chains. Combined with SPENDING_LIMIT policies, you can require human approval for large bridge amounts while allowing smaller automated operations.

The policy engine supports 21 policy types with 4 security tiers: INSTANT, NOTIFY, DELAY, APPROVAL. Bridge operations exceeding your delay threshold get queued for the specified delay period, giving you time to cancel if needed.

Multi-Chain Portfolio Monitoring

WAIaaS aggregates DeFi positions across all integrated protocols. One API call shows your complete cross-chain exposure:

curl http://127.0.0.1:3100/v1/defi/positions 
  -H "Authorization: Bearer wai_sess_<token>"

Response includes positions from Aave (Ethereum), Jupiter LP pools (Solana), Jito staking (Solana), Drift perpetuals (Solana), and other protocols — with USD values, health factors, and yield rates unified in one response.

Transaction Pipeline and Gas Optimization

WAIaaS runs a 7-stage transaction pipeline that handles bridge complexities automatically:

  • Stage 1-3: Validation, authentication, policy checks
  • Stage 4: Gas condition waiting — bridges execute only when gas meets your threshold
  • Stage 5: Execution with automatic retry logic
  • Stage 6-7: Confirmation monitoring across both chains

Gas conditional execution is crucial for bridges since they involve two transaction fees. Set your gas threshold to avoid executing during fee spikes:

# Transaction waits until Ethereum gas < 30 gwei
curl -X POST http://127.0.0.1:3100/v1/actions/across/bridge 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{
    "fromChain": "ethereum-mainnet",
    "gasCondition": {"maxGasPrice": "30000000000"},
    ...
  }'

Getting Started with Cross-Chain DeFi

Here’s how to set up cross-chain bridging in 5 minutes:

  1. Start WAIaaS with Docker:
git clone https://github.com/minhoyoo-iotrust/WAIaaS.git
cd WAIaaS
docker compose up -d
  1. Create wallets on multiple chains:
npm install -g @waiaas/cli
waiaas quickset --mode mainnet  # Creates Ethereum + Solana wallets
  1. Fund your wallets — deposit tokens you want to bridge

  2. Create bridge policies:

waiaas quickstart  # Sets up reasonable default policies
  1. Test a bridge operation using the REST API examples above

Your wallets are now ready for cross-chain DeFi automation. The 39 REST API route modules handle everything from bridge routing to transaction monitoring.

Advanced Bridge Features

For production systems, WAIaaS offers additional bridge capabilities:

  • Dry-run simulation: Test bridge operations without execution using "dryRun": true
  • Batch operations: Bridge multiple tokens in one transaction
  • Custom recipients: Bridge directly to different addresses on destination chain
  • Route optimization: LI.FI automatically finds cheapest/fastest bridge routes

The MCP integration provides 45 tools that AI agents like Claude can use directly — including bridge operations, balance checks, and position monitoring across all chains.

What’s Next

Cross-chain DeFi strategies are just getting started. WAIaaS gives you the infrastructure to build sophisticated AI agents that can arbitrage yield opportunities, rebalance portfolios, and execute complex strategies across the entire DeFi ecosystem.

Ready to build cross-chain DeFi automation? Check out the full documentation and examples at https://github.com/minhoyoo-iotrust/WAIaaS, or explore the live API reference at https://waiaas.ai.

Velocity Is Up. Ask Why Your Team Is So Quiet.

The engineer has been at the company three years. Senior. Knows the system well. Knew it, anyway. The codebase has been moving fast since the AI tools arrived. New services, new patterns, new surface area every sprint. The features are shipping. The metrics look clean. Someone in a leadership meeting said the word “10x” and nobody pushed back because the PRs are right there on the board, merged and closed. I was in those rooms. I said the right things about momentum.

Then something breaks. And the engineer sits down to debug it.

Six months ago, they would have known exactly where to look. Not because they’re exceptional. Because they wrote the relevant code. They had the context in their hands. The muscle memory was there. You build that over years of writing, breaking, fixing, and writing again inside the same system. It lives in you before you think to use it.

Now the code was generated. The patterns aren’t theirs. The structure is mostly right. And the engineer is staring at something they technically own that they don’t understand at the line level. So they do what you do in that situation. They reach for AI to help them debug it.

AI writes. Human reviews. Human can’t debug. Human asks AI. AI writes more.

That loop is what nobody named when the mandate came down.

The Shift Nobody Announced

The throughput is real. I’m not disputing the board.

What the board doesn’t show is what the work actually became.

Before AI, a senior engineer’s edge wasn’t just technical skill. It was orientation. Knowing where to go. When something broke and drug them out of bed, they didn’t reason from first principles every time. They had a map built from years of writing the same system. That map was the actual asset. The thing that made them genuinely senior.

AI adoption at speed is quietly erasing it.

AI is generating new surface area faster than anyone can internalize it. The senior engineer reviewing a PR today may have never touched that module. Because AI wrote it. Because the sprint was moving and there was no reason to slow down to build context when the model could generate the function in forty seconds.

The result is a codebase that is nominally owned by the team and practically understood by no one.

That’s not on the engineers. That’s what happens when you optimize for output faster than the team can absorb it.

The Cycle

An engineer receives an AI-generated function to review. They evaluate it. Structure, logic, edge cases. They approve it. Time was saved. Velocity moved.

Three weeks later something breaks in that function. The engineer who approved it opens the file. The code isn’t wrong, exactly. But it’s not theirs. They don’t have the mental model for why it’s structured the way it is. The instinct they would have had if they’d written it … the one that says “this will fail under this condition” … isn’t there. Because instinct comes from authorship. You can’t review your way into it.

So they reach for AI. Ask it to help trace the bug. AI generates a hypothesis. The engineer evaluates it. Maybe it’s right. Maybe it isn’t. Either way, another layer of generated content sits in the stack that nobody fully owns. The loop deepens.

I’m not just describing what I’ve watched happen on teams. This is something I’ve felt. When I go into a codebase to debug something I reviewed but didn’t write, the instinct I used to carry isn’t there. I reach for AI. The cycle continues. The more I’ve thought about it, the more I understand it’s not a skill gap. It’s a context gap. And context doesn’t transfer through review.

For many senior engineers, the contrast is sharp and getting sharper. Junior engineers are building their baseline in this environment. The new way of working is just the way working works. For the people who built their careers on knowing exactly where to go, that loss is unmistakable. They feel it every time they open a file they technically approved but don’t recognize at depth. They feel it in every debugging session where the first instinct is no longer instinct at all.

They just don’t have permission to name it.

Instinct comes from authorship. You can’t review your way into it.

What Leadership Sees

Leadership sees the board. PRs merged. Features shipped. Velocity at a number that would have seemed ambitious eighteen months ago. Someone says “AI is working.” Everyone nods.

The team is quiet.

Not the quiet of focus. Not the quiet of flow state. The quiet of people navigating something unfamiliar, on the fly, without a compass, and without permission to say it’s hard.

Engineers are producing output. They’re also making up the methodology as they go, session by session, because nobody told them how to review AI-generated code at the pace they’re being asked to review it. There’s no playbook for the verification load. There’s no training for the judgment work that replaced the writing work. There’s a mandate and a dashboard and a stand-up.

The engineers are building the plane while flying it. Leadership is measuring how fast the plane is going.

I’ve heard the same story from enough people in enough orgs to know this is not an edge case. It’s the pattern.

Why Mandates Make It Worse

An AI mandate doesn’t just add a tool. It adds the pressure of compulsion to the fog of unfamiliarity.

Engineers who are already figuring out how to do this are now figuring it out under an org-level instruction to move faster. The pressure is real. The permission to name what it costs is gone. You don’t raise your hand in an all-hands and say “I used to know this codebase and now I don’t” when the company just announced AI is a strategic priority.

The senior engineers feel it most. The ones who built their careers on knowing the codebase. On being the person people came to when something broke. On having that orientation so internalized it felt like instinct. They’re the quietest ones in the room. Not because things are fine. Because what they’re experiencing doesn’t fit the narrative, and they’re not going to be the person who slows down the momentum.

That silence is not adaptation. It’s people absorbing a loss they were never given language for, while being told the change is unambiguously good.

When the engineers who used to push back in planning stop asking why … when the people who questioned the framing of a problem start accepting it and jumping straight to solution … that’s not focus. That’s depletion showing up as compliance. The same signal I described in the cognitive load piece. The pattern doesn’t stop at the individual day. It moves into the team’s relationship with its own codebase.

That silence is not adaptation. It’s people absorbing a loss they were never given language for, while being told the change is unambiguously good.

What This Requires From You

I’m not arguing against the tooling. I use it. The gains are real.

What I’m arguing for is understanding what your team is actually inside of.

Your senior engineers are not struggling because they can’t adapt. They’re struggling because something was taken from them that nobody acknowledged. The muscle memory. The orientation. The knowledge of exactly where to go when something breaks at an hour when you’d rather be anywhere else. That was years of accumulated context. AI adoption at speed is outrunning the ability to rebuild it. And mandates don’t leave room to say so.

Before you read another velocity dashboard, ask your team one question. Not about output. About navigation.

When something breaks, do you know where to look? Are you going there because you know it, or because you’re using AI to find your way back in?

If the answer is the second one, you are not watching a team that’s adapting smoothly. You are watching a team in the middle of a transition they haven’t been given room to name.

Meet them there. Not with a policy update. Not with a new process. With a conversation. What has actually changed for you? What do you own that you didn’t write? What do you not know anymore that you used to? What does debugging feel like now compared to a year ago?

Those answers won’t show up in the sprint summary. They will show up in who’s still on your team in twelve months, and what shape they’re in when they are.

Those answers won’t show up in the sprint summary. They will show up in who’s still on your team in twelve months.

The Quiet Is the Data

The throughput is real. The sustainability is not something you can see on the board.

The work didn’t shrink. It shifted. From creation to judgment. From authorship to evaluation. From knowing exactly where to go to reaching for a tool that didn’t write the code you’re trying to understand. It is a different category of work, running at a different pace, on a team whose muscle memory was built for something else.

Your team went quiet after AI arrived. That quiet is not a sign things are going well. It’s a sign your team is carrying something they don’t have permission to put down.

Stop celebrating velocity. Start asking your team what they actually know anymore.

That question is harder than reading the dashboard. It’s the only one worth asking before you find out the hard way.

One email a week from The Builder’s Leader. The frameworks, the blind spots, and the conversations most leaders avoid. Subscribe for free.

PWC 366, Task 2, Valid Times

PWC 366 Task 2, Valid Times

Here we are at another Weekly Challenge, ticking away the moments that make up a dull day. Fritter and waste the hours in an off-hand way. Kicking around on a piece of ground in your home town, waiting for someone or something to show you the way. Here’s a way.

The Requirements Phase

You are given a time in the form ‘HH:MM’. The earliest possible time is ‘00:00’ and the latest possible time is ‘23:59’. In the string time, the digits represented by the ‘?’ symbol are unknown, and must be replaced with a digit from 0 to 9. Write a script to return the count of different ways we can make it a valid time.

  • Example 1: Input: $time = "?2:34", Output: 3

    • 02:34, 12:34, 22:34
  • Example 2: Input: $time = "?4:?0", Output: 12

    • Combinations of hours 04 and 14, with minutes 00, 10, 20, 30, 40, 50
  • Example 3: Input: $time = "??:??", Output: 1440

  • Example 4: Input: $time = "?3:45", Output: 3

    • 03:45, 13:45, 23:45
  • Example 5: Input: $time = "2?:15", Output: 4

    • 20:15, 21:15, 22:15, 23:15

The Design Phase

My first thought is that it would be fun to make some kind of iterator or generator that replaces each ? with its valid possibilities. My second thought is, “Nah, that seems like a lot of work.” And since the cardinal virtue of Perl programming is laziness, I moved on to something simpler.

There are 24 valid hours and 60 valid minutes. The total valid combinations is the cross-product of the two. Let’s go with that.

The Implementation Phase

sub validTime($time)
{
    state @hour   = '00' .. '23';
    state @minute = '00' .. '59';

    my ($h, $m) = map { s/?/./gr } split(/:/, $time);

    return (grep /$h/, @hour) * (grep /$m/, @minute);
}

Notes:

  • state variables — These reference lists only need to be set up once, and only in the scope of this function. That’s what state does.

  • '00' .. '23' — The .. sequence operator has the nice feature that if you want leading zeroes, you get them. Looking at you, bash.

  • split(/:/, $time) — Divide the input into its components. As in so many weekly challenge tasks, I’m assuming that the input has already been sanitized and is showing up here in a valid form.

  • map { ... } — Do something to both the hours and the minutes.

  • s/?/./gr — The something is to replace ? with . so that it becomes a regular expression. The ? is a meta-character, so it needs to be quoted. Adding the r flag yields the modified string; otherwise s///g would yield the number of substitutions, which is not useful in this context.

  • my ($h, $m) = ... — Declaring and initializing two variables from a list of two things.

  • (grep /$h/, @hour) — Select a list of the valid hours that can match the $h pattern. This is in a scalar context (numerical multiplication), so the scalar value will result — the number of matches. Similarly for minutes.

  • We only need the count, so multiply the two. We could have generated the list of valid times by using the hours and minutes returned from grep in array context, but (I believe I already mentioned this), lazy.

The Delivery Phase

And there you have it. You run and you run to catch up with the sun, but it’s sinking, and racing around to come up behind you again. The sun is the same in a relative way, but you’re older, shorter of breath, and one day closer to death. But at least you solved weekly challenge 366.