🔌 The “Connect Wallet” Button Replaced My Entire Auth System

Here’s something I didn’t expect when I started building on Solana: I never wrote a signup form.

No username field. No password validation. No “confirm your email” flow. No “forgot password” page. Every piece of auth infrastructure I’d built dozens of times in Web2 — gone.

In its place: one button. “Connect Wallet.”

🧠 What a Wallet Actually Does

If you’re new to Solana, I wrote about the conceptual shift — from usernames to keypairs — in a previous post –

The short version: your identity is a cryptographic keypair, not a database row.

But for a developer, the practical implication matters more than the theory. Here’s what changes:

You don’t manage authentication. The wallet does. When a user clicks “Connect Wallet,” their browser extension (Phantom, Solflare, etc.) pops up a confirmation. They approve. Your app gets their public key. You never see the private key. You never store a password hash. You never issue a session token.

You don’t build onboarding flows. In Web2, every step of signup is a drop-off point. Email entry, email verification, profile setup, terms acceptance. On Solana, the user already has an identity before they visit your app. Your job is just to detect it.

“Logging in” is just signing a message. Your app sends a message, the wallet signs it, you verify the signature on your backend. No session cookie. No JWT. The signature is the proof.

⚠️ What You Lose

This isn’t strictly better. There are tradeoffs:

  • No password reset. If a user loses their private key, they lose access. There’s no admin tool to restore their account.
  • No account recovery. In Web2, you can email a reset link. On Solana, the keypair is the only identity. Lose it, and everything associated with that address — tokens, NFTs, program state — is inaccessible.
  • No gradual onboarding. In Web2, you can let users browse before requiring login. On Solana, the user’s address is their identity from the first interaction. There’s no “anonymous session” that converts to a registered account later.

I covered the technical details of keypair generation and storage — why extractable: true matters, how PKCS8 works — in another post –

But the developer workflow shift is what I had to actually rebuild in my head.

💻 The Code Difference

In Web2, auth looks like this:

// Signup
app.post("/signup", async (req, res) => {
  const hash = await bcrypt.hash(req.body.password, 10);
  await db.users.create({ email: req.body.email, passwordHash: hash });
  req.session.userId = user.id;
});

// Login
app.post("/login", async (req, res) => {
  const user = await db.users.findByEmail(req.body.email);
  if (!user || !(await bcrypt.compare(req.body.password, user.passwordHash))) {
    return res.status(401).send("Invalid credentials");
  }
  req.session.userId = user.id;
});

On Solana, auth looks like this:

// Connect wallet (browser side)
const { accounts } = await wallet.features["standard:connect"].connect();
const userAddress = accounts[0].address;

// That's it. The user is now "authenticated."

No password hashing. No session management. No database lookups. The wallet handles the cryptographic proof. Your app just reads the public key.

🎯 Why This Matters

The “Connect Wallet” pattern isn’t just a UI trend. It represents a fundamental shift in how identity works. In Web2, you build auth into your app. In Solana, auth is infrastructure — it lives in the wallet, not in your codebase.

That means as a developer, you don’t need to worry about password storage, session hijacking, or email verification. But you also can’t rely on support tickets or account recovery when something goes wrong.

Once I understood this tradeoff — less code, more responsibility — the “Connect Wallet” button stopped looking like a simplification and started looking like a completely different approach to identity.

Next: Transactions aren’t HTTP requests. They’re signed checks — and failed ones still cost you.

This is a submission for the 100 Days of Solana Writing Challenge, running from 15 May to 22 May.

Understanding Apache Kafka: A Beginner’s Guide to Real-time Data Streaming

Apache Kafka is a technology that is mainly used to build high-performance, real-time data pipelines and streaming applications. It handles vast quantities of data and this article aims to demystify Kafka for beginners. We will explain its core concepts and show its practical application through a real-time weather data processing example.

What is Apache Kafka?

Apache Kafka is an event streaming platform that is distributed, scalable, and fault-tolerant. Think of it as a highly efficient, persistent message queue that allows different applications to communicate by sending and receiving data streams. Kafka facilitates a publish-subscribe model where data producers send messages to a central system, and data consumers can read these messages independently.

Key Components of Apache Kafka

To understand how Kafka works, it’s essential to grasp its fundamental components:

1. Producers
Producers are client applications that publish (write) data records (messages) to Kafka topics. They are responsible for creating new data and sending it to the Kafka cluster. For instance, in our weather data example, the Python script fetching weather information from an API acts as a producer.

2. Consumers
Consumers are client applications that subscribe to (read) data records from Kafka topics. They process the data streams published by producers.

3. Brokers
Brokers are the core servers that form the Kafka cluster. Each broker is a Kafka server that stores data, handles requests from producers and consumers, and replicates data for fault tolerance.

4. Topics and Partitions
Topics are categories or feeds to which records are published. They are logical channels for organizing data streams. For example, open_weather_data would be a topic for all weather-related messages. Topics are further divided into partitions, which are ordered, immutable sequences of records.

5. Zookeeper (or Kraft in newer versions)
Historically, Kafka relied on Apache ZooKeeper for managing the cluster’s metadata. In newer versions of Kafka, Kraft has been introduced to remove the dependency on ZooKeeper, simplifying the architecture.

Building a Real-time Weather Data Processing Pipeline with Kafka

Let’s illustrate these concepts with a practical example: a real-time weather data processing pipeline using the provided Python code. This pipeline demonstrates how producers fetch data, publish it to Kafka, and consumers then process it.

Producer Code Explanation

import requests
import json
from kafka import KafkaProducer
import time
from dotenv import load_dotenv
import os

load_dotenv()

api_key = os.getenv("API_KEY")

def get_weather_data():

    cities = ["Nairobi", "Mombasa", "Kisumu", "Eldoret", "Nakuru"]

    city_list = []

    for city in cities:

        url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}"

        data = requests.get(url)

        raw_data = data.json()

        city_list.append(
            {
                "city": city,
                "temperature": raw_data["main"]["temp"],
                "humidity": raw_data["main"]["humidity"],
                "description": raw_data["weather"][0]["description"],
                "last_update": raw_data["dt"]
            }
        )

    return city_list

producer = KafkaProducer(
    bootstrap_servers='localhost:9092',
    value_serializer = lambda p:  json.dumps(p).encode('utf-8')
                            )


while True:
    weather_data = get_weather_data()
    topic = 'open_weather_data'
    producer.send(topic, value=weather_data)
    print(f"Producer: {weather_data}")
    time.sleep(5)

  • The script fetches current weather data for a list of Kenyan cities from the OpenWeatherMap API.

  • It then extracts relevant weather details (city, temperature, humidity, description, last update timestamp) and returns them as a list of dictionaries.

  • We then specify the address of the brokers (localhost:9092) that the Producer will connect to.

  • value_serializer : Defines how the data sent to Kafka should be serialized. In our case, it converts the Python dictionary containing the weather data to json and then encodes it to utf-8, which is the format Kafka expects.

  • The while loop ensures we run the above steps continuously.

  • We then define a topic as, ‘open_weather_data’ and use producer.send(topic, value=weather_data) to publish the fetched weather data to the topic defined.

  • In our code, we’ve defined a pause of 5 seconds before fetching the next batch but this can be adjusted accordingly.

Consumer Code Explanation

from kafka import KafkaConsumer
import json

consumer = KafkaConsumer(
    'open_weather_data',
    bootstrap_servers='localhost:9092',
    value_deserializer = lambda m: json.loads(m.decode('utf-8')),
    auto_offset_reset='earliest'
)

for message in consumer:
    print(f"Consumer: {message.value}")

  • ‘open_weather_data’ specifies the topic from which the consumer will read messages.

  • bootstrap_servers = ‘localhost:9092’ is the same as for the producer, pointing to the Kafka broker(s).

  • value_deserializer = lambda m: json.loads(m.decode(‘utf-8’)) defines how the received data (value) from Kafka should be deserialized. It decodes the UTF-8 bytes back into a JSON string and then parses it into a Python dictionary.

  • auto_offset_reset = ‘earliest’ : ‘earliest’ means the consumer will start reading from the beginning of the topic (the earliest available offset). Other options include ‘latest’ (start from the most recent messages) or ‘none’ (throw an error if no valid offset is found).

Conclusion

Apache Kafka provides a robust and scalable solution for handling real-time data streams. By understanding its core components—producers, consumers, brokers, topics, and partitions—and seeing how they interact in a practical example like our weather data pipeline, you can begin to appreciate its power. This setup allows for efficient, decoupled communication between different parts of an application, enabling real-time data processing and analytics that are vital in today’s fast-paced digital world.

FCIS in Godot

This is a blog post in my FCIS in Godot series.

A problem I find when architecting large game development projects is the eventual tight-coupling between game logic and individual actors.

Game logic becomes tangled with scene Nodes, making it unportable.

This leads me to a mess of systems, leaving me without a library of portable scripts I can drop into any project, this is something I hope to change.

Functional Core

In this new functional game development architecture we draw a line in the sand between our functional and importantly: non-side effecting core code base and the much more granular imperative shell where we make our connection to the real world (via drawing on the screen and taking input from the user).

We are going to make sure our core game logic has zero reference or dependence on the Godot runtime what so ever. We can then make sure our “core”, core game logic is free of side-effects and made up of pure functions.

Implementation

Let’s now see how we can implement FCIS in Godot.

State Event Transformation

The functional core is made up of pure functions (systems) which take an initial, immutable, state and some event and return a new state.

The most high-level example of this transformation we could implement is;


// Core/WorldState.cs

// Some record which stores information about the current state of our game.
// With a read-only initialiser to ensure immutability.

public record WorldState 
{
    public static readonly WorldState New = new();
}

// Core/GenericEvent.cs

// Some event we can extend later.

public abstract record GenericEvent;

// Core/Simulation.cs

// The main transformation and entry point to our core game logic.
// This system will likely call transformations on other systems,
// i.e. a player-movement system, in a pipeline-like fashion.

public static WorldState Update(WorldState state, GenericEvent event) {}

Imperative Shell

This is where we make the bridge between our game logic and the game engine.

We will keep this as minimal and as ‘dumb’ as possible, as this is the code most coupled with our current project and most likely the code we will need to re-write in each new game we create.

We can simply call the functions WorldState.New and Simulation.Update to enter our game logic.

// ... some script attached to a Node

private WorldState _state = WorldState.New;

public override void _Process(double delta)
{
    if (Input.IsActionPressed("ui_up")) 
    {
        _state = Simulation.Update(_state, new GenericEvent());
    }
}

Counter Example

I can see how this still might also seem confusing and abstract, so let’s work through a very simple game.

Our game will simply display a counter on the screen (a Label Node), and if the user clicks the ↑ arrow, the counter increments, and if the user clicks the ↓ arrow the counter decrements.

Folder Structure

In a new project I will create two new folders; core/ and shell/.

Core

In core/, I will create three new scripts; CInputEvent.cs, WorldState.cs and Simulation.cs.

CInputEvent.cs

namespace Core.Events;

public abstract record CInputEvent;
public record Increment : CInputEvent;
public record Decrement : CInputEvent;

WorldState.cs

namespace Core.State;

public record WorldState(int Counter)
{
        public static readonly WorldState New = new(0);
}

Simulation.cs

using Core.Events;
using Core.State;

namespace Core;

public static class Simulation
{
        public static WorldState Update(WorldState state, CInputEvent input) => input switch
        {
                Increment => state with { Counter = state.Counter + 1 },
                Decrement => state with { Counter = state.Counter - 1 },
                _ => state
        };
}

Note the nice, modern pattern matching you can do in C# 7.0+

Shell

In our shell/ folder I’ll create one script to attach to a root Control Node in our main scene.

GameRoot.cs

using System.Collections.Immutable;
using Core;
using Core.Events;
using Core.State;
using Godot;

namespace Shell.Nodes;

public partial class GameRoot : Control
{
    private WorldState _state = WorldState.New;
    [Export]
    private Label _label = null;

    public override void _Ready()
    {
        Render();
    }

    public override void _Process(double delta)
    {
        var inputs = GatherInputs();
        if(inputs.IsEmpty) return;

        _state = inputs.Aggregate(_state, Simulation.Update);
        Render();
    }

    private static ImmutableList<CInputEvent> GatherInputs()
    {
        var events = ImmutableList.CreateBuilder<CInputEvent>();
        if (Input.IsActionJustPressed("ui_up")) events.Add(new Increment());
        if (Input.IsActionJustPressed("ui_down")) events.Add(new Decrement());
        return events.ToImmutable();
    }

    private void Render()
    {
        _label.Text = $"Counter: {_state.Counter}";
    }
}

All that is left to do is create a scene in Godot with a Label, attach the shell script.

Portability

In our example game, I now want to display a new counter with the same behaviour but in a 3D screen, and to be incremented when the user presses a different button.

In a tightly coupled implementation I would have to completely rewrite the counter’s behaviour again to cater specifically to this new scenario.

However, because our game logic is completely decoupled, the counter’s behaviour is ready to be reused where ever we might need it, and the more imperative action of detecting which button the user has pressed is trivial to implement.

GameRoot3D.cs

using System.Collections.Immutable;
using Core;
using Core.Events;
using Core.State;
using Godot;

namespace Shell.Nodes;

public partial class GameRoot3D : Node3D
{
    private WorldState _state = WorldState.New;
    [Export]
    private Label3D _label = null;

    public override void _Ready()
    {
        Render(); 
    }

    public override void _Process(double delta)
    {
        var inputs = GatherInputs();
        if(inputs.IsEmpty) return;

        _state = inputs.Aggregate(_state, Simulation.Update);
        Render();
    }

    private static ImmutableList<CInputEvent> GatherInputs()
    {
        var events = ImmutableList.CreateBuilder<CInputEvent>();
        if (Input.IsActionJustPressed("ui_enter")) events.Add(new Increment());
        return events.ToImmutable();
    }

    private void Render()
    {
        _label.Text = $"Counter: {_state.Counter}";
    }
}

Conclusion

You should be able to see how our new game architecture is helping us solve the issues of portability and extensibility by completely decoupling our game logic from whatever the current specific game project we are working on at the time.

In the above example there is clearly little time and effort saved, however if you imagine larger systems implemented in this way across an entire game the power of this architecture becomes apparent.

In the coming blog posts I will be using this framework in a real game example and document my progress and findings getting a player/enemy system up and running.

Why AI Infrastructure Code Fails After 6 Months – Even When The Demo Worked

Most AI demos fail for boring reasons.

Not because the model stopped working.

Not because the architecture was wrong.

Usually because the surrounding infrastructure was treated like temporary code.

The first version works in staging. Everyone is happy. The AI response looks good. The dashboard works. The API calls succeed.

Then 6 months later:

  • Queue workers are stuck
  • Retry loops are duplicating records
  • Context storage is inconsistent
  • Token usage exploded
  • Logs are impossible to trace
  • One vendor silently changed response formatting
  • Nobody wants to touch the integration layer anymore

We see this pattern a lot when AI systems move from experiments into permanent operation.

The problem is that most teams still build AI systems like feature launches instead of operational infrastructure.

The Demo Phase Hides Infrastructure Problems

In early development:

  • Low traffic
  • Small datasets
  • Few edge cases
  • Short prompts
  • Manual monitoring
  • One environment
  • One client
  • One model

Everything feels stable.

Then production happens.

Now the system runs continuously:

  • Thousands of requests
  • Multi-step workflows
  • External APIs timing out
  • Different client configurations
  • Long-term memory storage
  • Version drift between services
  • Human operators depending on outputs

This is where temporary architecture starts collapsing.

The Real Problem Usually Starts Around State

Most AI systems today are stateful whether teams admit it or not.

The moment you add:

  • conversation history
  • retrieval systems
  • workflow orchestration
  • memory
  • agent actions
  • async processing

you are no longer building a simple API wrapper.

You are building distributed infrastructure.

One issue we hit recently was inconsistent retrieval context across workers.

The vector database was healthy.

The embeddings were correct.

The prompts were valid.

But async jobs were reading stale state because cache invalidation timing was different between services.

The AI output looked “random” to users.

The actual issue was infrastructure consistency.

AI Failures Rarely Look Like Traditional Failures

Traditional backend failures are easier to spot:

  • 500 errors
  • crashes
  • failed queries
  • high latency

AI infrastructure failures are slower and messier.

Examples:

  • degraded answer quality
  • partial context injection
  • duplicated memory
  • token truncation
  • hallucinations caused by stale retrieval
  • silent schema mismatches
  • prompt formatting drift

The dangerous part is that systems still appear operational.

Requests succeed.

But output quality slowly degrades.

Those failures survive longer because monitoring is usually focused on uptime instead of reasoning quality.

Vendor Instability Changes Everything

A lot of teams underestimate this.

External AI providers change behavior constantly:

  • response formatting
  • tokenization
  • latency
  • rate limits
  • model quality
  • safety filtering
  • tool calling structure

If your infrastructure assumes provider consistency, production becomes fragile fast.

We started treating model providers the same way we treat unstable third-party integrations.

That means:

  • strict schema validation
  • response normalization layers
  • retry isolation
  • fallback handling
  • output sanity checks
  • version pinning where possible

Without that layer, small upstream changes leak directly into production behavior.

Long-Term Systems Need Operational Code

There is a difference between code that works and code that survives.

Operational AI systems need things most demos ignore:

Traceability

You need to answer:

  • Which prompt version generated this output?
  • Which retrieval documents were injected?
  • Which worker processed the request?
  • Which model version responded?
  • What was the token usage?
  • What changed between successful and failed runs?

Without deep tracing, debugging becomes impossible after scale.

Replayability

One thing we started building early:

Ability to replay full AI execution chains.

Not just logs.

Actual reconstruction of:

  • prompts
  • retrieval state
  • tool outputs
  • model responses
  • orchestration decisions

Because production AI bugs are hard to reproduce otherwise.

Failure Isolation

One bad external dependency should not corrupt the entire pipeline.

We now isolate:

  • embedding generation
  • retrieval
  • model execution
  • memory updates
  • workflow actions

as separate recoverable stages.

That changed system stability more than prompt optimization ever did.

The Biggest Mistake

The biggest mistake is assuming the AI model is the product.

In enterprise systems, the model becomes one component inside a much larger operational environment.

The infrastructure around it matters more over time:

  • orchestration
  • observability
  • recovery
  • consistency
  • deployment safety
  • data integrity
  • monitoring

The model can improve next month.

Broken infrastructure compounds for years.

LinkedIn vs Resume: What Should Match, What Should Differ, and Which Matters More?

Most job seekers treat their LinkedIn profile and resume like twins — nearly identical documents wearing the same clothes. I used to do the same thing, and it cost me opportunities I didn’t even know I was losing.

Here’s the thing: your resume and your LinkedIn profile have completely different jobs. Treating them like duplicates wastes the strengths of both. Here’s what I’ve learned about keeping them aligned without making them identical.

They’re Different Documents for Different Situations

Your resume is an application document. It exists to get you past an ATS and into a recruiter’s shortlist for one specific role. It should be targeted, tight, and optimized for speed reading.

Your LinkedIn profile is a discovery and credibility document. It’s designed to help people find you, understand you quickly, and feel confident enough to reach out, refer you, or pass your name along. It supports passive opportunities, referrals, and networking — not just direct applications.

This difference changes everything about how you should build each one.

What MUST Be Consistent

When people ask “should LinkedIn match my resume?”, this is the core of the answer: the facts must match, even if the framing doesn’t.

These should be identical across both:

  • Job titles (or near-equivalent titles)
  • Employment dates
  • Employer names
  • Degree, school, and graduation year
  • Your core career direction

If your resume says “Software Engineer” and your LinkedIn headline reads “Problem Solver | Builder | Tech Enthusiast,” you’re creating a credibility gap. Recruiters compare documents. Contradictions kill trust fast.

Where They Should Differ

This is where most candidates under-leverage one or both assets.

Your LinkedIn can and should include more:

  • A broader About section with personality and narrative
  • Featured media — portfolio links, project demos, presentations
  • Recommendations from managers and colleagues
  • A wider skill surface area for search discoverability
  • Posts, articles, and thought contributions

Your resume should stay tighter:

  • Only the most relevant experience for the target role
  • Fewer bullets, but stronger bullets
  • Selective keyword alignment to the job description
  • No narrative padding — just evidence and proof

The mental model: LinkedIn goes wide, resume goes deep on what’s most relevant right now.

The Summary Problem

One of the highest-leverage differences is how you handle the summary section on each.

Your resume summary should be short, role-specific, and use language close to the job description:

“Data Analyst with 3 years of experience building SQL-based reporting pipelines, Tableau dashboards, and stakeholder-facing business analysis for fintech and SaaS products.”

Your LinkedIn About section can be slightly broader and more human:

“I turn messy data into decisions that actually get used. My background is in SQL, Tableau, and business analysis — mostly in fintech and SaaS — and I’m especially interested in the gap between what data teams build and what business teams actually act on.”

Same positioning, different tone. The resume wins the screen. The LinkedIn section builds the relationship.

A Real Example That Shows Why This Matters

Let’s say you’re a Data Analyst targeting analytics roles. Your resume is crisp, targeted, and ATS-optimized. But your LinkedIn headline still says “Business Operations Specialist | Strategy | Process Improvement” from your previous role.

What happens? A recruiter sees your resume, gets interested, searches you on LinkedIn, and immediately gets confused about what you actually want to do. The doubt is small — but it’s enough to slow down the yes.

Fix: align your LinkedIn headline to match your target role family. “Data Analyst | SQL, Tableau, Dashboard Automation” is clear, searchable, and reinforces your resume instead of conflicting with it.

Which One Matters More?

The honest answer: it depends on the step.

Resume wins when you’re applying directly. LinkedIn wins when recruiters are sourcing or when someone’s checking you out after seeing your application.

Most candidates lose on the second scenario. They tailor the resume, submit, and never notice that their LinkedIn profile is outdated, vague, or tells a different story. Recruiters often check LinkedIn within minutes of seeing an interesting resume — and that check either reinforces or undermines everything the resume built.

The 5 Most Common Mistakes

1. Making them identical — you waste what each format does well.

2. Letting them tell different career stories — different emphasis is fine, different direction is not.

3. Using a vague LinkedIn headline — if your headline doesn’t make your target role obvious, discovery suffers.

4. Updating the resume but leaving LinkedIn stale — recruiters check LinkedIn right after they see applications.

5. Fixing LinkedIn aesthetics before fixing resume quality — if the resume is weak, a polished LinkedIn won’t save a direct application.

What to Fix First

If you’re applying actively this week, fix the resume first. If you want referrals, recruiter outreach, or passive opportunities, fix LinkedIn first.

For most people in an active job search, the best order is:

  1. Fix the resume baseline
  2. Tailor the resume to the job description
  3. Align your LinkedIn headline and About section to the same direction

The goal isn’t for them to say the same things. The goal is for them to reinforce the same story — so that anyone who sees both walks away with a clear, consistent picture of who you are and what you want to do next.

Read the full article here

Been using ManyOffer to sharpen my own answers — if you want AI mock interviews with real LP feedback, they have a deal running through July worth checking out: Claim 1 free month here

How I Built a WiFi Motion Detection App Using RSSI in Flutter

WiFi signals are everywhere around us.

But most people only think of WiFi as internet connectivity.

What many don’t realize is that WiFi signals can also be used for experimental human sensing — including motion detection, presence estimation, breathing monitoring, and activity analysis.

Recently, I started building an experimental Flutter application called:

SenseWave: WiFi Motion Detector

The app explores how RSSI (Received Signal Strength Indicator) fluctuations can be analyzed in real time to estimate environmental changes caused by human movement.

This project combines:

  • Flutter
  • Android native WiFi scanning
  • RSSI signal analysis
  • Realtime visualization
  • Signal processing
  • Motion analytics

without using:

  • Camera
  • Microphone
  • Bluetooth
  • External sensors

What Is RSSI?

RSSI stands for:

Received Signal Strength Indicator

Every WiFi packet received by your device has a signal strength value.

Example:

-45 dBm → Strong signal
-72 dBm → Weak signal

When humans move inside a room, they affect:

  • signal reflections
  • attenuation
  • multipath propagation
  • interference patterns

These small fluctuations can be analyzed to estimate motion and environmental activity.

What My App Can Detect

The app is experimental, but currently focuses on:

1. Presence Detection

Estimate whether someone is inside a room by analyzing RSSI variance and signal instability.

Use cases:

  • smart home occupancy
  • room activity monitoring
  • automation triggers

2. Motion Detection

Human movement causes rapid RSSI fluctuations.

The app analyzes:

  • variance
  • signal spikes
  • temporal instability

to estimate:

  • movement intensity
  • room activity level
  • motion events

3. Approximate Position Heatmap

Using RSSI changes from nearby access points, the app can generate an approximate activity heatmap.

Important:
This is NOT precise indoor positioning.

It is an experimental approximation based on signal disturbance.

4. Breathing Detection (Experimental)

Breathing creates micro signal fluctuations.

Using:

  • FFT analysis
  • low-pass filtering
  • periodicity detection

the app attempts to estimate breathing rate under controlled conditions.

Best results:

  • single person
  • short distance
  • minimal interference

5. Sleep Monitoring

The app can also analyze:

  • breathing stability
  • nighttime motion
  • movement interruptions

to create experimental sleep activity analytics.

6. Walking Pattern Recognition

Walking generates periodic RSSI peaks.

By analyzing:

  • frequency patterns
  • step periodicity
  • motion rhythm

the app estimates:

  • walking activity
  • movement intensity
  • approximate step frequency

Signal Processing

RSSI data is noisy.

To improve stability, I use:

  • moving average filters
  • FFT
  • Kalman filtering
  • variance analysis
  • peak detection

These help extract meaningful patterns from unstable WiFi signals.

Why This Technology Is Interesting

WiFi sensing could eventually enable:

  • smarter homes
  • contactless monitoring
  • occupancy automation
  • low-cost environmental sensing

without requiring cameras or wearable devices.

The combination of:

  • RF sensing
  • mobile apps
  • realtime signal processing
  • machine learning

opens many exciting possibilities.

Final Thoughts

Building SenseWave has been one of the most interesting Flutter projects I’ve worked on.

It combines:

  • mobile engineering
  • RF sensing
  • signal processing
  • realtime visualization
  • experimental AI concepts

all inside a Flutter application.

WiFi signals contain much more information than most people realize.

And we are only beginning to explore what’s possible.

Follow My Project

App:

SenseWave: WiFi Motion Detector


Exploring realtime RSSI-based human sensing using Flutter and Android.

What is Infrastructure as Code and Why It’s Transforming DevOps

I’ve been learning cloud technologies for a while, but there’s always been one thing that slows you down when managing infrastructure manually. Mistakes, inconsistencies, and time wasted configuring servers. That’s where Infrastructure as Code (IaC) comes in. For the next 30 days, I’ll be diving into Terraform as part of the 30-day Terraform challenge, and I want to share what I learn along the way.

What is Infrastructure as Code?

Infrastructure as Code is simply managing and provisioning your infrastructure through code rather than manual configuration. Instead of clicking around in the AWS console to create servers, networks, or databases, you write code that defines what you want.

The big advantage is consistency. Every time you run your code, you get the same environment. No more missing security settings, misconfigured instances, or forgotten network rules. Everything is repeatable, version-controlled, and reviewable.

Declarative vs Imperative Approaches

Imperative: You give step-by-step instructions. For example, “create a server, install software, configure settings.”

Declarative: You declare the final state you want, and the system figures out how to get there. For example, “I want a server with NGINX installed and this network configuration.”

Write on Medium
Terraform is declarative, which means I focus on describing what I need, and it handles all the steps behind the scenes. This makes deployments faster, safer, and easier to scale.

Why Terraform is worth learning

Terraform is cloud-agnostic, widely used, and integrates well with AWS, Azure, GCP, and more. Learning Terraform means I can confidently build infrastructure across different cloud platforms. It also makes it easy to collaborate with other engineers, because the infrastructure is just code.

For someone like me who wants to grow in my devops/cloud engineering career, mastering Terraform is a clear advantage. It’s the skill that lets me move from experimenting to delivering real solutions for businesses.

My goals for this 30-day challenge is to build tangible skills I can showcase. I want to:

  • Understand Terraform deeply, not just superficially
  • Build practical projects I can showcase on GitHub and portfolio
  • Learn how to manage infrastructure efficiently on AWS
  • Share my journey so others can follow along and maybe learn a few things from my mistakes

Conclusion

Today was all about setting up my environment and understanding the foundations. The next days will be about writing actual Terraform code, experimenting with AWS resources, and turning ideas into real infrastructure.

I’m excited to continue, and I’m looking forward to sharing progress, lessons, and challenges along the way. If you’re curious about Terraform or IaC, stay tuned.

Build personalized email campaigns per customer

This guide walks through a workflow I used recently i.e. connect Orshot to Cursor, turn a reference image into a parameterized template, render a unique hero image per customer, and send it through your email provider. You stay in the editor for design and exploration and production sends use the API the same way any other integration would.

By the end of this guide, you will have a studio template with named parameters (customer_name, headline, hero_image, and so on), a render call that returns a hosted image URL per row of customer data, and a small script that combines that URL with an email send.

Prerequisites

  • An Orshot account
  • A Cursor setup
  • Whatever you already use for transactional email (Resend, AutoSend, etc.). A CSV or database of customers is enough for the last step.

What I was actually trying to do

Before the steps, it helps to be explicit about the loop, because each section maps to one part of it.

You start with a reference image (a newsletter you liked, a competitor layout, a crop from a mood board). In Cursor you ask the agent to build something similar in Orshot and to mark which layers should change per recipient. Those marks become API parameters. For each customer you pass different values into the same template, get back an image URL, and embed that URL in the email body.

Getting the MCP connected

I love being in Cursor all day and so I set up Orshot’s MCP server to avoid interacting with the console manually for updation, deletion and other functions. These are the steps I followed:

  • Sign in at orshot.com.
  • Go to Generate → Agents & MCP → Cursor → Install in Cursor. That adds the Orshot MCP server to Cursor’s configuration.
  • In Cursor, click “Connect” the MCP server when prompted, then complete authorization in the Orshot dashboard in the browser tab that opens.

  • Verify the install: open Cursor’s MCP settings and confirm tools are listed, including orshot_create_template_design, orshot_generate_image, orshot_batch_render, and orshot_get_studio_template_modifications, etc.

Turning a reference image into a template

You do not need to open Orshot Studio first. The flow below is what worked for me – reference in chat, create a simple layout, refine, then parameterize.

Step 1. Bring the reference into Cursor

Paste or attach your inspiration image in the chat. In the same message, describe the layout and the output size. Email heroes are often around 600px wide so pick a width and height you will keep for the whole campaign to avoid re-rendering at multiple sizes later.

The prompt that works for me always:

Here is a reference for an email hero. Build an Orshot template in the same spirit. Canvas 600×400. Keep the first version simple — background, headline, image area, CTA only.

Step 2. Let the agent create, refine and parameterize

The agent will typically call orshot_create_template_design for a first pass, then orshot_update_template_design for spacing, type, and polish. Asking for a perfect layout in a single call often produces a muddier result than create-then-refine so treat the second pass as a normal step.

Here’s a prompt that worked for me:

Mark headline, hero_image, customer_name, and cta_label as parameters. List all modification keys for this template and give me the template ID.

Once you are through with the changes, look at the parameters summarized by Cursor to ensure that it’s the way you want them to be for your template.

Step 3. Render a test image

Here’a a prompt that allows me to render my template for custom values:

Render template [ID] with customer_name “Alex”, headline “Hi, Alex”, hero_image [public URL], cta_label “View account”. Return a PNG URL.

Open the URL in a browser. If the layout is wrong, iterate in chat (orshot_patch_template_elements for small text or style tweaks, or another orshot_update_template_design for bigger changes). The template also appears in Orshot Studio if you want to adjust pixels by hand.

Once the template exists, the rest is mostly data

After the design is stable, each send is the same operation with different modifications. Keys must match the parameterId values from the previous section.

For dozens or hundreds of customers, loop in your own code against the REST API instead of pushing everything through chat. Keep the same modifications object per row and add a queue, retries, and logging as you would for any outbound job.

Step 1. Prepare customer data

Each row needs at least an email address and the values for your template parameters (name, plan, image URL, etc.). A CSV, database export, or CRM webhook all work as long as you can map columns to modification keys.

Step 2. Render, then send

The pattern is always to render → image URL → HTML body → send. Example with the Node SDK (swap in your email client):

import { Orshot } from "orshot";

const orshot = new Orshot(process.env.ORSHOT_API_KEY);

const TEMPLATE_ID = 12345;

type Customer = {
  email: string;
  name: string;
  plan: string;
  heroUrl: string;
};

async function sendPersonalizedHero(customer: Customer) {
  const { url: imageUrl } = await orshot.renderFromTemplate({
    templateId: TEMPLATE_ID,
    modifications: {
      customer_name: customer.name,
      headline: `${customer.name}, your ${customer.plan} summary`,
      hero_image: customer.heroUrl,
      cta_label: "View dashboard",
    },
    responseType: "url",
    responseFormat: "png"
  });

  await emailProvider.send({
    to: customer.email,
    subject: `Your ${customer.plan} update`,
    html: `
      <p>Hi ${customer.name},</p>
      <img src="${imageUrl}" width="600" alt="" style="display:block;max-width:100%;" />
      <p><a href="https://app.example.com">Open your account</a></p>
    `,
  });
}

Step 3. Verify end to end

Run one real row, pull name and plan from your spreadsheet, render, send to your own inbox, and confirm the image loads in Gmail (or whatever client your audience uses).

What I would change if I did it again

These are not requirements, but they would have saved me time on a second run.

I would decide which fields are dynamic before the layout is final, so parameter names never diverge from what I expect. I would pick canvas dimensions once and avoid re-rendering at multiple scales unless a second channel really needs it.

For work where brand fidelity is non-negotiable, you will still want a designer in the loop!

Ending Thoughts

What helped me most was dropping the repetitive export step of settling the layout, marking what should change (name, headline, image, CTA), and after that you only pass in different values and paste the URL into the message, same structure every time without opening a design file for each person. You do not need to be a designer for that part either, you can describe what you want in plain language and work from a screenshot you like, the agent and MCP handle the layout work well enough for onboarding emails, renewals, and other campaigns where “good enough and consistent” beats hand-polishing every asset.

Code Tracker AI A Developer‑First Productivity Tool (Join the Beta Waitlist)

If you’ve ever wished your IDE could quietly help you understand your coding patterns, reduce context switching, and surface insights without spying on you or slowing you down, you’re exactly who I built this for.

I’m a solo founder and engineer, and after months of building, testing, and refining, I’m opening the beta waitlist for Code Tracker AI — a privacy‑first JetBrains plugin designed to help developers work smarter, not harder.

🧠 What Code Tracker AI Does
Code Tracker AI runs inside your IDE and gives you:

Real‑time coding insights without sending your code to external servers

Pattern detection to help you understand how you work

Lightweight analytics that stay local and respect your privacy

AI‑powered summaries of your coding sessions

Zero‑setup onboarding — install, code, and get insights instantly

This isn’t another “productivity dashboard.”
It’s a tool built by a developer, for developers who want clarity without surveillance.

🔒 Privacy First, Always
No tracking scripts.
No hidden data collection.
No selling developer behavior.
Your code stays yours — period.

🧪 Beta Testers Wanted
I’m opening the beta waitlist for developers who want early access and are willing to give feedback that shapes the final product.

If you want to be part of the first wave:

👉 Join the waitlist: https://codetrackerai.com

👉 Be part of building a tool that actually respects developers

💬 Why I’m Building This
Developer tools shouldn’t feel like corporate monitoring software.
They should empower us, not watch us.

Code Tracker AI is my attempt to fix that — and I’d love your help making it even better.

If you’re curious, have ideas, or want to collaborate, drop a comment.
Let’s build something developers actually want to use.

For developers & made by a developer

Ramona (Mona) Hidalgo

AI Coding Tip 020 – Create a Second Brain

mcsee profile

Build a Persistent Memory Layer Your AI Can Actually Use

TL;DR: Use Obsidian with Markdown notes, YAML metadata, and direct AI file access to build a Second Brain with LLMs that gives your tools persistent project context.

Common Mistake ❌

You paste a snippet of code or a problem description into an AI chat, get a good answer, and close the tab.

The next week you face the same problem again.

You search your chat history, find nothing useful, and paste the same snippet again.

Your AI starts fresh every session with zero memory of what you built, decided, or debugged before.

Problems Addressed 😔

  • You repeat yourself constantly because context resets between sessions and your AI remembers nothing.
  • You lose architectural decisions, debugging notes, meeting commitments, and design rationale after each conversation ends.
  • Your AI gives generic answers because it lacks the specific context of your project and past decisions.
  • Mental load accumulates as you juggle multiple projects, notifications, and context switches simultaneously.
  • You lose track of decisions made weeks ago without a structured retrieval system.
  • As you age or face cognitive overload, you forget context between sessions, repeat past mistakes, and lose the thread of long-running projects.
  • Notification fatigue builds when every system demands your attention in real time instead of on your schedule and you forget what you were searching for.

How to Do It 🛠️

  1. Choose a local Markdown tool like Obsidian, Logseq, Foam, or Tolaria to store all your knowledge as plain .md files you own and control.

  2. Organize your vault using the PARA system: Projects (active work with deadlines), Areas (ongoing responsibilities), Resources (reference material), and Archives (inactive items).

  3. Write atomic Zettelkasten notes: one idea per file, with a unique identifier and explicit links that explain why two concepts relate, not just what they are.

  4. Add YAML front matter to every note so your AI gets searchable, structured metadata it can query and filter:

---
title: "JWT Authentication Design"
type: architecture-decision
status: approved
component: auth-service
tags: [security, backend]
created: 2018-12-09
related: ["OAuth Flow", "Session Management"]
---
  1. Use Dataview or Obsidian Bases to query your vault like a database and surface all tasks due today, pending decisions, or notes grouped by component with a single dynamic query; every note is a record with metadata and legible text.

  2. Give your AI direct access to all your vault as local files so Claude, Devin, and Codex can read your entire codebase and notes simultaneously.

  3. Create a AGENTS.md in each sub-project that points to the relevant vault sections, following the nested agents pattern from AI Coding Tip 014.

  4. Dictate notes on the go using voice input and paste the transcript into your vault to capture context in real time without interrupting your flow.

  5. Sync your vault to mobile and tablet using Obsidian Sync, Dropbox, OneDrive, or Syncthing so your Second Brain travels with you everywhere.

  6. Set up OpenClaw or a local LLM (Ollama or LM Studio) to index your vault and answer questions via Telegram or WhatsApp, as a private assistant that never sends your data to the cloud.

  7. Ask your AI to scan your vault periodically for orphaned notes, missing connections, and hidden patterns, acting as a tireless librarian that reorganizes your knowledge network over time without falling into vault-design paralysis.

  8. Bootstrap your vault from your existing knowledge: ask ChatGPT, Claude, Grok, or Gemini to summarize and export your most relevant past conversations as Markdown notes, since you already have a Second Brain scattered across chat history waiting to be harvested.

Benefits 🎯

  1. Persistent Context: Your AI starts every session knowing your architecture, past decisions, and current priorities instead of starting blank.
  2. Pattern Discovery: The LLM finds connections between notes you wrote months apart and surfaces forgotten solutions to current problems.
  3. Focus Protection: You capture every interruption into your inbox and process it on your schedule, eliminating notification fatigue.
  4. Memory Extension: You can retrieve exact decisions and reasoning from months ago without relying on biological memory alone, making it especially valuable for senior developers, people with attention challenges, or anyone whose cognitive load has grown beyond what one brain can hold.
  5. Privacy by Default: When you use local models like Ollama with OpenClaw, your notes, code, and queries never leave your machine.
  6. Compounding Knowledge: Every note you add makes your Second Brain denser: the more you use it, the more valuable it becomes.
  7. Mobile Access: You can query and update your knowledge base from any device, making your Second Brain available wherever you work.
  8. Distraction-Free Writing: You work in plain text files with minimal formatting, much like zenware, so you stay focused on ideas instead of tools.
  9. Visual Graph: You can open Obsidian’s Graph View to see all your notes as a visual network where nodes are notes and edges are links, revealing clusters, hub concepts, and hidden connections at a glance.

Context 🧠

Tiago Forte’s “Building a Second Brain” methodology introduced the CODE framework: Capture, Organize, Distill, Express.

For developers, this maps directly to how you learn, document, and reuse knowledge across projects.

Niklas Luhmann, a sociologist who wrote over 50 books, developed the Zettelkasten method using a physical slip box of interconnected atomic notes.

Your Obsidian vault is that slip box, and your AI can search, analyze, and extend it at any time.

LLMs have three types of memory: parametric (knowledge encoded during training), working (the current context window), and external (your files on disk).

Your Second Brain fills the external memory layer and gives the AI permanent access to your project-specific knowledge that survives every session reset.

Just as neural networks in LLMs store patterns in weights, your Second Brain stores patterns in linked Markdown files, extending the model’s memory with your lived experience as a developer.

Andrej Karpathy captured this architecture in one sentence: “Obsidian is the IDE; the LLM is the programmer; the wiki is the codebase.”

The front matter you add to each note acts as structured metadata that significantly improves how AI retrieves and ranks relevant information.

RAG (Retrieval-Augmented Generation) pipelines embed your notes as vectors and return the most semantically similar ones when you ask a question, injecting them directly into the AI’s context window.

I borrowed this idea many years ago from @ernesto Kiszkurno and have written more than 500 technical articles since then.

Prompt Reference 📝

Bad Article 🚫

Create a note about the movie
"Howl's Moving Castle".

Good Article 👉

---
title: "Howl's Moving Castle"
type: movie-note
status: draft
director: Hayao Miyazaki
year: 2004
genre: [fantasy, animation, anti-war]
rating: 9/10
tags: [ghibli, miyazaki, adaptation]
related: ["Studio Ghibli", "Diana Wynne Jones"]
created: 2026-05-16
expected publish date: 2026-06-12
---

## Poster
![Howl's Moving Castle](Howls-moving-castleposter.jpg)

## Summary
Two or three lines describing the plot without
spoiling the ending.

## Themes to Explore
Bullet list of ideas: anti-war message,
identity and transformation, Sophie's arc,
Miyazaki's pacifism compared to the novel.

## Characters
Use wikilinks: [Howl], [Sophie Hatter],
[Calcifer], [The Witch of the Waste].

## Article Ideas
Numbered list of angles for a critical article:
compare Miyazaki's changes from the Diana Wynne
Jones novel, analyze the anti-war allegory,
explore the feminist reading of Sophie's curse.

## Related Notes
[Studio Ghibli filmography]
[Diana Wynne Jones bibliography]
[War themes in animation]

Considerations ⚠️

Keep your notes in plain Markdown files, not HTML (despite current trends) or proprietary formats.

Markdown is readable by both humans and LLMs, diffs cleanly in Git, and works with every static site generator.

Don’t use MCP connectors or remote APIs to give your AI access to your vault.

Point it directly at local files instead.

Direct file access is faster, cheaper, and requires no extra infrastructure.

The AGENTS.md files you create per project are part of your Second Brain, not separate from it.

If you have privacy concerns about cloud AI providers reading your notes, run a local model with Ollama or LM Studio through OpenClaw.

Your data never leaves your machine or private cloud.

Mind maps and Canvas diagrams in Obsidian help you visualize codebase architecture and sprint planning spatially, while the linked Markdown notes behind each node stay searchable by your AI.

Your files are local.

Nobody can see them, nobody can index them, and you can open any note with the simplest text editor in the world.

If you use a cloud AI model instead of a local one, you can add a guardrail to restrict what files the AI can read, so sensitive notes never leave your control.

Type 📝

[X] Semi-Automatic

Limitations ⚠️

Large vaults with thousands of notes can slow down semantic search and local LLM indexing on low-end hardware.

Front matter requires discipline to maintain consistently: inconsistent metadata reduces query quality over time.

Local LLMs powerful enough to reason over a large vault require significant RAM (16 GB minimum, 32 GB recommended).

Tags 🏷️

  • Knowledge Management

Level 🔋

[X] Intermediate

Related Tips 🔗

Conclusion 🏁

Your biological brain is for having ideas, not storing them.

Build a Second Brain from local Markdown files and give your AI direct access to it.

You get persistent context, pattern discovery, and a knowledge base that compounds every time you use it. 🧠

More Information ℹ️

Zettelkasten Introduction

Obsidian – Local Markdown Knowledge Base

// Detect dark theme
var iframe = document.getElementById(‘tweet-2039805659525644595-193’);
if (document.body.className.includes(‘dark-theme’)) {
iframe.src = “https://platform.twitter.com/embed/Tweet.html?id=2039805659525644595&theme=dark”
}

Front Matter in Real books

Dataview Plugin for Obsidian

Andrej Karpathy LLM Wiki Pattern

Foam – Second Brain for VS Code

Building a Second Brain – Tiago Forte

Second Brain (Spanish)

Also Known As 🎭

  • Personal-Knowledge-Management
  • AI-Memory-Extension
  • Developer-Second-Brain
  • Local-Knowledge-Base

Tools 🧰

Disclaimer 📢

The views expressed here are my own.

I am a human who writes as best as possible for other humans.

I use AI proofreading tools to improve some texts.

I welcome constructive criticism and dialogue.

I shape these insights through 30 years in the software industry, 25 years of teaching, and writing over 500 articles and a book.

This article is part of the AI Coding Tip series.