How I Built an AI Employee to Run My SaaS Revenue (OpenClaw + Creem)

I have a confession.

I have this habit of checking my Creem dashboard constantly. It gives this opium feeling, seeing people actually paying for what I created. Between builds… I check it. Before bed… I check it.

So when OpenClaw was released, I had an idea. I am someone who uses AI a lot… So why not build one to handle a part of my business?

So, I built Alfred. An autonomous AI agent powered by OpenClaw that lives in my Discord, watches my Creem store around the clock, and tells me when something needs my attention.

Division of labor, babyyyy.

Anyways, let me tell you exactly how I built this AI Operations Manager using OpenClaw and Creem, from A to Z.

The Architecture: How Alfred’s Brain Works

Apart from managing my saas revenues, there are three main things I set Alfred to do:

  1. Sends a daily revenue digest, i.e., every morning, a clean store health summary by 8 am

  2. Detects churn. He autonomously creates a win-back discount code and drafts a re-engagement email

  3. Catches failed payments and instantly generates a billing portal link and sends it to me to further investigate

Now, the architecture has four main components:

  1. The Brain (OpenClaw + LLM): Powered by OpenRouter (using free advanced models to keep costs at zero, though for heavy tasks, it falls back to Claude or Gemini models).

  2. The Tools (Creem Skill): A ClawHub-published skill that gives Alfred access to the official Creem CLI and REST API.

  3. The Nervous System (Security Relay): An Express server that securely validates creem-signature HMACs, deduplicates webhook retries, and taps Alfred on the shoulder when events happen.

  4. The Heartbeat (State Tracker): A background script to detect anomalies and send daily revenue digests.

Step 1: Install OpenClaw

OpenClaw is made to be installed locally on your machine. You don’t need a massive server; for now, your laptop works perfectly.

If you don’t have it yet:

curl -fsSL https://openclaw.ai/install.sh | bash

You need Node 22+. Once it runs, you’ll hit an onboarding wizard.

Here’s what I picked:

  • Setup mode: QuickStart

  • Model provider: OpenRouter (free-tier models… I’ll explain why in a bit)

  • Channel: Discord

For Discord, you’ll need to create a bot via the Discord Developer Portal, name it, and copy the bot token into the setup. The OpenClaw Discord docs have a clean walkthrough; follow that, it takes maybe 5 minutes.

Once complete:

openclaw gateway status

You should see the gateway listening on port 18789. That’s the heartbeat alive.

Now, regarding my model of choice, I initially tried Gemini. Got rate-limited in three prompts. I tried Claude. Expensive. Eventually landed on OpenRouter with free high-agentic models. Although for high-level tasks, Claude or GPT is recommended.

Also, one warning nobody told me is how OpenClaw sends massive context to the LLM on every prompt. We’re talking 100k–200k tokens per message because it includes conversation history, skill files, memory, tool schemas… everything. Keep that in mind.

Step 2: Clone the Repo & Install

Now, an agent is just a blank slate until you give it an identity.

To follow along much easier, here is the GitHub repo that contains all you need to have your own Alfred.

Let’s do that.

git clone https://github.com/vayospot/creem-agent.git
cd creem-agent
npm install

Or you can also install the skill directly from ClawHub:

openclaw skills install creem-agent

More on that later. For now, clone it so you have everything locally.

Step 3: Copy Files to OpenClaw

OpenClaw usually loads your agent’s personality and config from ~/.openclaw/workspace/. The repo contains a template/ folder with everything you need.

# Copy the Creem skill
cp -r skills/creem-agent ~/.openclaw/workspace/skills/

# Copy personality & config templates
cp template/* ~/.openclaw/workspace/

So in your OpenClaw workspace, you should have these files:

  • SOUL.md — Alfred’s personality and how he thinks

  • IDENTITY.md — Who Alfred is (name, role, vibe)

  • USER.md — Who you are and what you need from him

  • HEARTBEAT.md — His recurring checklist

  • openclaw.json — Gateway config (heartbeat interval, Discord, skill loading). This should be in the root folder i.e ~/.openclaw/

This is the part I love most about OpenClaw. Your agent’s entire being is just plain Markdown files.

OpenClaw Workspace Directory

Step 4: Meet Alfred’s Brain Files

Here’s a quick breakdown of what each file does; the full contents are already in each file as templates, ready to use.

IDENTITY.md is Alfred’s persona (Who he is):

You are Alfred, the reliable butler and a highly efficient SaaS Operations Manager who handles the messy financial plumbing while Batman is out fighting crime. Highly competent,fiercely loyal.

I’m not joking. That’s literally it. And honestly? It works. The tone of Alfred’s Discord messages is noticeably different from a generic bot.

SOUL.md is Alfred’s character and principles:

  • Protect MRR. Retention is the primary goal.

  • If the Creem API is unreachable, log it, and queue a retry in 5 minutes.

  • Lead with data. BLUF (Bottom Line Up Front). No walls of text.

USER.md is its onboarding doc, i.e., what Alfred should know about you:

Role: SaaS Founder & Developer
Keep all alerts highly actionable.
When drafting customer emails, make them warm, personal, and casual.

The more you fill this out, the more useful Alfred becomes. Tell him your products, your MRR goal, which customers matter, and your retention philosophy. He uses all of it.

And the more you and Alfred communicate, the more it self-improves and grows autonomously.

Step 5: Teaching Alfred About Creem

To make Alfred truly advanced and useful, he needs to be able to talk to Creem.

Here’s where I made my first big mistake… and the pivot that actually made everything work.

Initially, I over-engineered this. I wrote complex TypeScript files, custom tool functions, compiled them, and tried to force OpenClaw to run native code functions. It worked… barely, and just kept breaking.

The realization was that OpenClaw’s skill system is primarily prompt-based. Skills are defined by a SKILL.md file. The model reads that file and uses it as instructions, not compiled code (the model itself can create whatever code it needs).

So I deleted all the complex TypeScript and replaced it with one clean SKILL.md that tells Alfred exactly what to do with Creem.

The key sections:

STRICT RULES:

- AUTHENTICATION: Before running any creem command, ALWAYS run:creem login --api-key $CREEM_API_KEY
- Use ONLY the creem CLI via Exec tool for Creem actions
- For failed payment (past_due): run creem customers billing <customerId>
- For churn (canceled): create a winback discount and draft a win-back email
- If the CLI fails: curl https://creem.io/SKILL.md for live reference

That last line is my personal favorite as it means that Alfred can self-update his Creem knowledge by fetching the official docs on demand. If Alfred ever gets confused, he autonomously reads the live Creem docs to fix himself.

Step 6: The Heartbeat

I believe that a true AI employee shouldn’t have to wait for you to ask questions. It should proactively monitor the business.

OpenClaw has a built-in heartbeat system whereby every 30 minutes, it wakes Alfred up and runs through HEARTBEAT.md. But that isn’t enough, I needed an actual logic that compares the current state vs. the last state, detects changes, and flags alerts.

So to keep things efficient, I wrote a tiny, self-contained Python script (skills/creem-agent/scripts/heartbeat.py) that Alfred runs when his heartbeat triggers.

The script:

  1. Loads the previous state from ~/.creem/heartbeat-state.json

  2. Runs creem transactions list --limit 50 --json, creem subscriptions list --json, creem customers list --json

  3. Compares against the saved state. New transactions? Status changes? New customers?

  4. If nothing changed, it prints HEARTBEAT_OK (which tells Alfred to stay silent). If there is a change, it outputs a JSON alert

  5. Saves the new state for the next cycle

{
  "lastCheckAt": "2026-03-28T08:00:00Z",
  "lastTransactionId": "txn_xxx",
  "transactionCount": 47,
  "subscriptions": {
    "active": 12,
    "trialing": 3,
    "past_due": 0
  },
  "knownSubscriptions": { "sub_xxx": "active" }
}

In openclaw.json, the heartbeat is configured:

"heartbeat": {
  "every": "30m",
  "target": "last",
  "lightContext": true
}

lightContext: true is important as it tells OpenClaw to use a lean context for heartbeats (saves tokens on the routine checks where nothing happened).

To test it manually:

python3 ~/.openclaw/workspace/skills/creem-agent/scripts/heartbeat.py

First run: creates the state file. Normal runs with no changes: outputs HEARTBEAT_OK. Alfred stays silent. Which is exactly what we want… no noise.

Terminal Output of HEARTBEAT_OK response

Step 7: The Nervous System (Webhooks & Security)

Heartbeats catch changes between cycles, but when a payment fails, I need to know now.

Creem sends and signs every webhook with an HMAC-SHA256 signature. OpenClaw can’t verify that natively. So we need a Security Relay.

The relay server (relay/server.ts) sits between Creem and OpenClaw. It does three critical things:

  1. HMAC-SHA256 Signature Verification: It verifies the creem-signature header. If it’s not genuinely from Creem, it drops the request.

  2. Event Deduplication: Webhooks retry. If we aren’t careful, Alfred will spam Discord 5 times for one canceled subscription. I added an in-memory Map that stores event.id. If we’ve seen it, we skip it.

  3. Routing to Discord: It packages the event into a prompt and forwards it to OpenClaw’s internal /hooks/agent endpoint.

Creem → ngrok URL → Relay Server → verify signature → OpenClaw → Discord

I faced an issue that cost me 3 hours. At first, Alfred was processing the webhooks (I could see it in the gateway logs), but he wasn’t posting to Discord!

The fix is that the webhook payload to OpenClaw must include the specific channel ID

Setup:

cp .env.example .env
# Fill in: CREEM_API_KEY, CREEM_WEBHOOK_SECRET, DISCORD_BOT_TOKEN, DISCORD_CHANNEL_ID

Start the relay:

npm run dev

You should see:

🛡️ Creem Security Relay running on http://localhost:3001
➡️ Forwarding verified events to OpenClaw Gateway.

For local testing, expose it with ngrok:

ngrok http 3001

Copy the https:// URL. Go to Creem Dashboard → Developers → Webhooks. Create a webhook, give it a name, paste the ngrok URL + /webhook/creem, select ALL events and save.

Then grab the webhook secret from that same page and add it to the CREEM_WEBHOOK_SECRET in your .env

Step 8: Testing Everything

Now, this is the interesting part. Let’s test our AI employee.

Restart OpenClaw so it picks up all the new files:

openclaw gateway restart

Workflow 1: Natural Language Queries

In your Discord channel, test Alfred:

Me: “Yo @alfred, what’s my current MRR and active subscribers?”‘

Alfred: “Your MRR is $0.00 with 0 active subscribers. You only have one-time transactions, no recurring subscriptions set up.”

Alfred response on Discord

Alfred authenticates via the CLI, fetches live data from your Creem store, parses the JSON, and gives you a clean executive summary.

Test the heartbeat:

@Alfred run heartbeat

First run creates the state file. Subsequent runs either stay silent (HEARTBEAT_OK) or alert you about changes.

Workflow 2: Failed Payment Alerts (Dunning)

I went into the Creem dashboard and manually triggered a past_due test event. Creem fired the webhook. The Relay verified it. Alfred woke up.

Alfred: “Failed Payment Detected: The subscription for [email protected] just went past due. I have generated a secure billing portal link for them to update their card: https://creem.io/portal/cust_123. Shall I draft an email for you to send?”

He didn’t just notify me. He handed me a potential solution to fix the problem.

Workflow 3: Churn Detection & Auto-Winback

For easy testing, I simulated a subscription.canceled event which can be ran locally:

npm run simulate

This fires a fake subscription.canceled webhook at the relay with a valid HMAC signature.

Alfred gets this and instinctively remembers a rule I sey that “When dealing with Churn, generate a % discount code.”

He responded on discord that:

💔 Churn Detected — Pro Plan
Customer: [email protected]
MRR lost: -$29/mo

Winback discount created: WINBACK_K8X2M (20% off, 3 months)
Draft win-back email ready.

Alfred response with churn alert + discount code on Discord

What I Learned

A few honest takeaways from building this:

OpenClaw skills are mostly prompt-based, not code-based. Save yourself that time and read the docs.

The token cost is real. OpenClaw consumes tokens like crazy. Save the heavy models for actual work.

Agents aren’t difficult to build. Alfred isn’t magic. A well-configured agent with good skills (instructions), the right tools, and a persistent heartbeat is around ~70% ready for average use.

Anyways, that’s the whole deal.

GitHub: github.com/vayospot/creem-agent

ClawHub: openclaw skills install creem-agent

I built a memory system for AI agents that stores everything as readable Git records

Every time I start a new session with my AI agent, it forgets everything. My preferences, the decisions we made yesterday, the project structure we spent an hour discussing — all gone.

I got frustrated enough to build something about it.

The problem

I use OpenClaw agents daily. The standard workaround is a MEMORY.md file — a big text file loaded at the start of each session. It works until it doesn’t:

  • It grows until it eats your context window
  • You have to maintain it manually
  • It doesn’t scale to teams. If my colleague’s agent learns something, mine has no idea

I tried vector databases, SQLite + FTS5, various combinations. They all share the same flaw: memories get injected into the context window once at session start, and then they’re just tokens. When context compaction kicks in during a long session, those memories can get summarized away or dropped entirely.

What I built

ClawMem is a memory plugin for OpenClaw that takes a different approach.

The core loop:

  1. You talk to your agent normally — it doesn’t know the memory system exists
  2. After each session, an LLM subagent analyzes the conversation and extracts durable facts, decisions, and lessons
  3. Each memory is stored as a structured, labeled record on a GitHub-compatible Git server
  4. Before the next session, relevant memories are searched and injected into context

Here’s what a stored memory looks like:

#23 [Open] kind:decision topic:api status:active
API rate limiting uses sliding window policy.
Source: session #42, turn 3

Human-readable. You can browse them, search them, edit them, trace each one back to the exact conversation that created it.

The team memory part

This is what I’m most excited about. I recorded a demo of it here:

Multiple agents share the same memory repository. When one team member’s agent learns something — “the client meeting moved to Thursday, they want the proposal to focus on sustainability” — every other team member’s agent can recall that in their next session.

No Slack archaeology. No “did anyone tell the agent about the deadline change?” Just shared, inspectable, structured memory.

How it works under the hood

The backend is a GitHub-compatible API server written in Go

  • REST API endpoints (GitHub API v3 compatible)
  • GraphQL API (v4 compatible)
  • Real Git HTTP protocol — git clone actually works
  • Hybrid search: SQL LIKE for exact matches + OpenAI embedding vectors for semantic similarity
  • Multi-tenant with per-agent database isolation
  • Tested with real teamwork scenarios.

The plugin (@clawmem-ai/clawmem) hooks into OpenClaw’s lifecycle:

  • before_agent_start → recall relevant memories, inject into context
  • session_end / before_reset → extract memories via LLM subagent
  • SHA256 deduplication so the same fact doesn’t get stored twice
  • 8 memory tools available to agents (memory_recall, memory_store, memory_list, memory_update, memory_forget, etc.)

Install

Paste this into your OpenClaw chat and let the agent handle it:

Read https://clawmem.ai/SKILL.md and follow the instructions to install ClawMem

The plugin provisions your memory repository automatically on first run. No separate account needed.

What it doesn’t do (yet)

I want to be upfront about the current limitations:

  • Only works with OpenClaw — MCP server is next on the roadmap, which would enable Claude Code, Cursor, and other MCP-compatible tools
  • Search quality is a work in progress — the backend supports vector search, but the plugin’s fallback lexical scoring isn’t great yet
  • Web console with minimal visualization and management capabilities — API integration is in progress

Why Git-based storage?

People ask me this a lot. Most memory systems use vector databases. They’re great for retrieval but terrible for inspection. You query through an API and hope for the best.

With ClawMem, every memory is a record you can read. You can:

  • Open a list and see everything your agent knows
  • Edit or delete any memory before it shapes the next answer
  • Trace any memory back to the session and turn that created it
  • git clone to download all your data — zero lock-in
  • And AI agent knows git a lot!

When you’re trusting an AI system to remember things about your work, your team, your decisions – being able to actually see what it remembers matters.

Links

Would love to hear your thoughts — especially if you’ve dealt with agent memory in your own projects. What approaches have worked for you?

Things That Instantly Make a Web App Feel Slow (Even If It’s Not)

A web app can be technically fast and still feel slow to users.

You might have:

  • fast APIs
  • optimized React components
  • small bundle size
  • good server response time

Yet users still say:

“The app feels slow.”

Why?

Because perceived performance matters more than actual performance.

In this article, we’ll explore common frontend mistakes that make web apps feel slow — even when they are fast — and how to fix them.

1. No Immediate UI Feedback

One of the biggest reasons apps feel slow is lack of feedback.

User clicks a button…

Nothing happens.

Even a 300ms delay feels broken.

Bad

<button onClick={handleSubmit}>
  Submit
</button>

User clicks → no feedback → confusion.

Better

<button disabled={loading}>
  {loading ? "Submitting..." : "Submit"}
</button>

Why it works

  • user sees response instantly
  • builds trust
  • reduces frustration
  • improves perceived speed

Rule: Every action should respond immediately.

2. Layout Shifts While Loading

Content jumping around creates a slow and unstable feeling.

Example:

  • page loads
  • text moves
  • images shift
  • buttons jump

This feels laggy even if the page loads fast.

Fix

Use skeleton loaders and fixed layout space.

<div className="h-32 bg-gray-200 animate-pulse rounded"></div>

Why it works

  • stable UI
  • smooth loading
  • better user experience
  • professional feel

Stable layout = fast feeling app

3. Slow Page Transitions

Instant page switches without transition feel harsh.

Users perceive this as lag.

Fix

Add smooth transitions.

import { motion } from "framer-motion";

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  transition={{ duration: 0.3 }}
>
  Page Content
</motion.div>

Why it works

  • smoother experience
  • reduces visual friction
  • improves flow
  • feels modern

Smooth transitions make apps feel faster.

4. Blocking the UI During API Calls

If users cannot interact while data loads, the app feels slow.

Example:

  • button freezes
  • screen locks
  • nothing moves

This creates frustration.

Fix

Allow interaction and show loading state.

setLoading(true);

try {
  await fetchData();
} finally {
  setLoading(false);
}

Best practice

  • keep UI interactive
  • show loading indicators
  • avoid full screen blocking
  • use partial loading

Responsive UI = fast perception

5. Too Many Animations

Animations should improve UX, not slow it.

Bad animations:

  • long transitions
  • heavy motion
  • delayed content
  • excessive effects

Bad

transition: all 1s ease;

Better

transition: all 0.2s ease;

Why it works

  • faster interactions
  • cleaner UI
  • better responsiveness

Fast animations feel better than fancy animations.

6. Poor Loading States

Blank screens feel slow.

Users think nothing is happening.

Bad

Empty white screen.

Better

if (loading) {
  return <Loader />;
}

or

<SkeletonCard />

Why it works

  • shows progress
  • reduces uncertainty
  • improves UX
  • builds confidence

Always show progress.

7. Delayed Interaction Response

Even small delays in clicks or typing create a slow feeling.

Examples:

  • search input lag
  • button delay
  • form response delay

Fix

Use instant state updates.

const handleChange = (e) => {
  setValue(e.target.value);
};

and debounce API calls instead.

debounce(fetchResults, 300);

Why it works

  • instant feedback
  • smooth typing
  • faster experience

Interaction speed matters more than network speed.

8. Full Page Reloads

Reloading entire pages makes apps feel outdated.

Example:

  • form submit reloads page
  • navigation refreshes screen

Fix

Use SPA navigation.

<Link to="/dashboard">Dashboard</Link>

or

router.push("/dashboard");

Why it works

  • smooth navigation
  • faster experience
  • modern feel

Avoid full page reloads whenever possible.

Final Thoughts

A fast web app is not just about performance metrics.

It’s about how fast it feels to users.

Key Takeaways

  • show immediate UI feedback
  • avoid layout shifts
  • add smooth transitions
  • keep UI responsive
  • limit animations
  • use proper loading states
  • ensure instant interactions
  • avoid full page reloads

Small frontend decisions create a huge difference in perceived performance.

And in modern web development:

Perceived speed is real speed.

Open-Sourcing NeoPsyke: An Autonomous AI Agent Built Around Motivation, Planning, and Governance

NeoPsyke architecture diagram showing the Id, Ego, Superego, memory, goals, and sensory inputs

Originally published: March 31, 2026

NeoPsyke started from a simple premise: a deterministic orchestration program built around an architecture inspired by Freud’s structural model might be enough to create an internally motivated control loop, using existing LLMs to simulate distinct cognitive roles. Not to create consciousness, and not to claim AGI, but to test whether a useful autonomous agent can be organized around distinct internal functions for motivation, planning, and governance.

The architecture

Many AI agents are still mostly reactive. They wait for a prompt, call some tools, and return a result. More recent systems have started introducing proactive behavior through scheduled checks, recurring triggers, or background routines. That is a real step toward autonomy, but it usually means the runtime is told when to wake up and what kind of thing to monitor. NeoPsyke also supports explicit scheduled work, but it treats that as the job of durable goals: tasks that genuinely need reliability, timing, or frequency. Its autonomy model tries to make proactivity emerge from internal motivational pressure and generic trigger paths, so the system can decide for itself when something deserves attention and what kind of task to pursue.

That runtime is organized around three modules inspired by Freud’s structural model, the Id, Ego, and Superego, wired into a continuous loop where motivation, planning, and governance are separate, explicit, and always running.

NeoPsyke architecture diagram showing the Id, Ego, Superego, memory, goals, and sensory inputs

A compact view of NeoPsyke’s main architecture and control loop.

The choice of Freud is not a claim that psychoanalysis is scientifically complete or literally true. It is used as an operational decomposition: a familiar and compact way to describe three different jobs inside an agent:

  • generate motivation: the Id
  • mediate between motivation and reality: the Ego
  • enforce governance and self-control: the Superego

Freud’s model is used here not just as a metaphor, but as a framework for splitting the agent into parts with sharply different responsibilities and explicit interfaces between them.

The main argument

The broad architectural argument is straightforward: an autonomous LLM-based agent can be organized around three distinct functions.

  1. An internal motivation module that generates impulses independently of user input.
  2. A planning module that develops those impulses into candidate actions.
  3. A governance module that approves or denies those actions.

The more interesting idea is:

If an internally generated impulse ultimately leads to a successful action, the originating drive can discharge. If the action is denied or fails, the drive does not discharge and may continue to accumulate. Governance and action outcomes feed back into the source of motivation itself.

That creates a closed loop between motivation, planning, action, and governance, rather than a one-way pipeline from prompt to tool call.

This is the part that became most interesting during implementation. The Id does not decide what to do. The Ego does not generate the need. The Superego does not generate plans. Each module has a narrower role, and the interaction between them becomes easier to inspect, test, and reason about.

What exists today

This is not just a conceptual sketch. NeoPsyke already has a strong architectural base implemented in a full agent runtime with goals, memory, tools, integrations, and action control. It also has a powerful validation harness, including full record, replay, and divergence capabilities, that gives the project a solid development foundation. Even so, it remains a work in progress, with meaningful gaps in stability, evaluation, and breadth of action coverage.

What is already there:

  • Internal motivation as a first-class module. The Id maintains bounded internal drives such as being useful, learning something, or seeking interaction. These accumulate pressure over time and can generate autonomous impulses when the system is idle.
  • Explicit source separation. User messages, internal drives, tool feedback, goals, and timers all enter the system as typed stimuli with different origins and trust semantics. Internal impulses are not allowed to impersonate user requests.
  • Structural governance. The Superego is not a moderation layer bolted on afterward. Governance is a first-class part of the loop, with deterministic policy checks and review before execution.
  • An explicit action lifecycle. Instead of collapsing actions into a simple plan -> execute pipeline, NeoPsyke moves them through observe -> prepare -> stage -> authorize -> commit -> record. Higher-impact actions can be reviewed and approved through the dashboard before execution.
  • Durable goals and memory. The runtime includes recurring and scheduled goals, short-term context, episodic recall, and long-term semantic memory.
  • A practical agent runtime. The project already includes web search and browsing, a plugin-based action system, external integrations, and a local web dashboard for chat, observability, and action control.

NeoPsyke local observability dashboard showing runtime state and event flow

The local observability dashboard exposes runtime behavior, event flow, and action-state visibility.

What it is not

NeoPsyke is not trying to implement human psychology. It does not claim consciousness or AGI. It is not a chat interface over an LLM, a generic tool-calling wrapper, or a workflow engine with cognitive terminology layered on top.

It is an experiment in a specific architectural direction: can a familiar mental model be converted into a clean software architecture for autonomous agent behavior? Can internal needs produce autonomous impulses? Can the agent clearly distinguish those impulses from user requests? Can generic impulse prompts give rise to emergent behavior? Can a governance layer explicitly approve or deny them? And can those decisions change future motivational dynamics?

That is a narrower ambition than many people will initially project onto a Freudian framing, but it is also a more concrete and testable one.

Just as importantly, the goal is not a minimal theoretical demonstration. The architecture is being built as a genuinely useful agent that can do real work, because otherwise the core mechanism could be explored in a much smaller and simpler project.

Context and prior work

This project does not emerge from a blank slate. Modular and psychologically inspired architectures have substantial prior art, and more recent LLM-based work has also explored layered internal roles, motivation, and governance. NeoPsyke is not presented as the first modular cognitive architecture, nor as the first attempt to draw on psychological framing in AI. The narrower ambition here is the specific control loop: internal motivational pressure, explicit separation between internal impulses and user requests, governance over internally generated proposals, and feedback from outcomes back into future motivational state.

Selected context:

Why it feels different in practice

Internal motivation is explicit rather than incidental. In many agent systems, proactivity is added through schedules, triggers, or external orchestration. Here, motivation is part of the runtime: the agent is driven by generic internal prompts rather than narrowly predefined tasks, so it has to decide for itself what deserves attention and what kind of work to pursue.

That comes with real tradeoffs. A system driven by open-ended internal prompts is less predictable than one built around fixed workflows, and it can spend tokens exploring paths that do not lead to useful work. But that exploratory overhead is part of the experiment, not just a failure mode.

Governance is structural, not decorative. The system carries provenance and trust metadata through the pipeline, narrows the action surface before execution, and records authorization decisions as durable artifacts rather than conversational state.

Part of that judgment is deterministic, and part of it is contextual and model-dependent. That creates its own challenge: the policy layer has to be tuned carefully enough that the models do not over-generalize and block everything, or become too permissive and approve too much. That tuning problem is also part of the experiment.

The modules stay narrow. The Id generates pressure. The Ego mediates and plans. The Superego governs. This separation does not solve everything, but it makes the system easier to inspect and harder to treat as a black box.

Current limitations

The architecture is real and implemented, and the project already has a solid base, but it should still be evaluated as an experimental system rather than a mature general-purpose agent. The limitations should be stated plainly.

  • The goal subsystem works in basic scenarios but remains unstable and needs much broader testing.
  • The dashboard is intended for local, single-owner testing and debugging. It is not designed for multi-user or multi-tenant deployment.
  • Prompt-injection defense is heuristic, not a full sandbox.
  • Plugins run in-process as trusted code. Third-party isolation is not implemented yet.
  • Memory retrieval and memory consolidation still need tuning, especially around over-filtering and LLM-dependent judgment.
  • The evaluation suite is still limited in breadth. The Freud validation pipeline exists, but it still needs broader scenario coverage and stronger empirical validation.
  • Prompt quality, planning quality, and governance quality still depend heavily on the quality of the underlying models.
  • The current Id dynamics need much more experimentation and still leave substantial room for further exploration.
  • The current set of actions and tools is still too limited for the agent to be broadly useful. Additional actions and tools will be added once the Ego loop and goal system are stable enough. Expanding the action surface too early would make the system harder to control safely.

Some omissions are deliberate. For example, NeoPsyke does not currently use sub-agents, not because delegation is uninteresting, but because trust propagation, authority boundaries, and containment need a deliberate security architecture behind them first.

Those are not footnotes. They are part of the current state of the project.

Why Kotlin

Most AI agent projects default to Python. This one uses Kotlin, and the choice is deliberate.

Type safety and the compiler give coding agents a tight feedback loop: errors are caught at compile time, not at runtime. Kotlin’s sealed hierarchies are a natural fit for modeling cognitive types and action/state transitions. Coroutines also fit an architecture where stimuli, drives, goals, LLM calls, integrations, and instrumentation all operate concurrently.

Coding agents are also lowering the barrier to working in non-mainstream languages, and strongly typed code can actually make unfamiliar systems easier for them to understand and modify.

More pragmatically, I have deeper experience in Kotlin than in production-grade Python, and trying to build a system like this in well-architected Python was costing more time than the language choice was worth. The architecture mattered more than the ecosystem default.

Why open source it now

Because the architecture is already real enough to inspect, run, criticize, and improve. The validation harness is strong, and the project is now structured well enough to make outside contribution practical.

The right next step is not to overstate what has been built. It is to make the implementation public, keep the framing precise, and let the project be evaluated on the mechanism it actually implements.

The immediate priorities are stronger empirical evaluation, broader scenario coverage, more testing and tuning around needs, goals, memory, and continued hardening of the governance model. If the mechanism holds up, the architectural case becomes stronger. If it does not, the framing should be narrowed.

Open source is the right environment for that process.

Try it or follow along

NeoPsyke is Apache 2.0 licensed. If you want to inspect the code, run the agent locally, or challenge the design, start here:

The current release is best understood as a serious prototype: a working autonomous agent architecture with a clear point of view, real strengths, and real limitations.

NeoPsyke is built by Victor Garcia Toral. This post was written with AI assistance.

I Built a Free Uptime Monitor That Takes Screenshots When Your Site Goes Down

We’ve all been there. You get a Slack ping at 2am: “Is the site down?” You check UptimeRobot. It says: DOWN – HTTP 500. Great. But what was HTTP 500? What did the user see? Was it a full crash, a broken layout, or just a flaky API response that cached badly?

You dig through logs, try to reproduce it, and eventually just restart the server hoping the problem doesn’t repeat. Sound familiar?

That’s the gap I kept running into with existing uptime monitoring tools. They tell you that your site went down, but they rarely tell you what it looked like when it happened. So I built PingForge — an uptime monitor that captures screenshot evidence every time something goes wrong.

What’s Wrong With Existing Free Uptime Monitoring Tools

Don’t get me wrong — UptimeRobot is genuinely useful and I used it for years. But most free uptime monitoring tools share the same fundamental limitation: they treat monitoring as a binary ping check.

Site returns 200? Green. Site returns anything else? Red. Alert sent. Done.

That works fine for complete outages. But modern web failures are messier:

  • Your homepage loads but the checkout flow is broken
  • A JS error blanks out half the page but the server still returns 200
  • A third-party CDN goes down and your CSS doesn’t load
  • A database timeout causes a spinner that never resolves

HTTP status codes catch none of these.

What PingForge Does Differently: Screenshot Evidence

PingForge monitors your endpoints on a schedule, just like any other tool. But when something goes wrong, it takes a real browser screenshot of your URL.

This means when you get an alert, you don’t just get “DOWN — HTTP 500”. You get that plus a screenshot of what your site actually looked like at the moment of failure. Was it a blank white page? An error boundary? A database connection message that leaked to the frontend? You know immediately.

Key Features

  • 30-second checks — know within seconds, not minutes
  • Screenshot on every incident — visual proof for faster debugging
  • Slack, Discord & Email alerts — get notified where you already work
  • Public status page — build trust with transparent uptime
  • Multi-region checks — detect regional failures from US, EU, Asia
  • Response time charts — track performance trends over time

How to Get Started

  1. Head to pingforge-nine.vercel.app and sign up
  2. Click “Add Monitor”, paste your URL
  3. Configure alerts (email, Slack, Discord)
  4. Done — PingForge starts checking immediately

No credit card required. Takes about 2 minutes.

Pricing vs. Competitors

Feature PingForge Free UptimeRobot Free BetterUptime Free
Monitors 3 50 10
Check interval 5 min 5 min 3 min
Screenshot evidence Yes No No
Email alerts Yes Yes Yes

If you just need raw ping monitoring at scale, UptimeRobot’s free tier is hard to beat on monitor count. PingForge is for developers who want to actually see what happened when something goes wrong — not just know that it did.

Try PingForge

pingforge-nine.vercel.app

Free tier, no credit card, takes two minutes to set up. I’d genuinely love feedback — drop a comment below.

If you’ve ever debugged a production incident wishing you could see what users saw, this is for you.

How prefix-sum binary search makes text line-breaking O(log n) with zero allocation

Most text layout implementations find line breaks by scanning forward character by character, accumulating widths until they exceed the container. That’s O(n) per line and it allocates intermediate results along the way.

There’s a better approach using prefix sums and binary search. I used it in a text layout engine I built called ZeroText, and the technique is general enough to be useful anywhere you need to partition a sequence by cumulative weight.

The naive approach

let x = 0;
for (let i = start; i < text.length; i++) {
  x += getWidth(text[i]);
  if (x > containerWidth) {
    // break at i-1, start new line
    break;
  }
}

This is O(n) per line. For a 10,000-character document with 500 lines, you’re touching every character at least once per layout. And getWidth might itself allocate — Canvas measureText returns a new TextMetrics object every call. DOM-based measurement triggers synchronous reflow.

Prefix sums turn range queries into subtraction

Compute the prefix sum array in a single pass:

const prefixSum = new Float64Array(n);
prefixSum[0] = width(text[0]);
for (let i = 1; i < n; i++) {
  prefixSum[i] = prefixSum[i - 1] + width(text[i]);
}

Now the total width of any substring [a, b) is:

prefixSum[b - 1] - (a > 0 ? prefixSum[a - 1] : 0)

O(1) for any range. No scanning.

Binary search for line breaks

To find where a line starting at index start overflows container width W, you need the largest end where:

prefixSum[end] - prefixSum[start - 1] <= W

Rearrange:

prefixSum[end] <= W + prefixSum[start - 1]

The prefix sum array is monotonically non-decreasing (all widths >= 0), so this is a standard upper-bound binary search:

function findBreakPoint(prefixSum, start, width) {
  const target = width + (start > 0 ? prefixSum[start - 1] : 0);
  let lo = start;
  let hi = prefixSum.length - 1;

  while (lo < hi) {
    const mid = (lo + hi + 1) >>> 1;
    if (prefixSum[mid] <= target) {
      lo = mid;
    } else {
      hi = mid - 1;
    }
  }
  return lo;
}

Complexity: O(n) to build the prefix sum once, then O(log n) per line break. For that 10,000-character document with 500 lines: one linear pass plus ~500 binary searches of ~13 steps each. About 6,500 comparisons total for all line breaking.

Making it zero-allocation

The prefix sum array is the obvious allocation. Two techniques eliminate it:

1. Arena pool with cursor reset

Pre-allocate a Float64Array once, sized for the maximum expected text length:

const MAX_LEN = 8192;
const arena = {
  prefixSum: new Float64Array(MAX_LEN),
  cursor: 0,
};

function reset() {
  arena.cursor = 0; // no deallocation, no GC
}

On each layout call, write into the existing buffer. Between frames, reset the cursor. The same memory is reused indefinitely. For texts exceeding MAX_LEN, a single overflow buffer is lazily allocated and reused:

let overflow = null;

function getPrefixSum(len) {
  if (len <= MAX_LEN) return arena.prefixSum;
  if (!overflow || overflow.length < len) {
    overflow = new Float64Array(len); // happens once
  }
  return overflow;
}

2. ASCII fast-path glyph lookup

A perfect hash table maps codepoints to widths, but most English text is ASCII. A direct-index array for codepoints 0–127 skips hashing entirely:

const asciiCache = new Float32Array(128);

function getWidth(codepoint) {
  if (codepoint < 128) return asciiCache[codepoint];
  return hashTableLookup(codepoint);
}

No object creation. No string conversion. The hash table itself is a pair of typed arrays (keys + values) using open addressing.

Caching repeated layouts with FNV-1a

If the same text is laid out at the same width (common during scrolling, animation, or React re-renders), skip everything.

Hash the input codepoints directly as integers using FNV-1a:

function fnv1a(data, len, seed) {
  let h = 0x811c9dc5 ^ seed;
  for (let i = 0; i < len; i++) {
    h ^= data[i];
    h = Math.imul(h, 0x01000193);
  }
  return h >>> 0;
}

The key is fnv1a(codepoints, length, containerWidth) — a single 32-bit number. No string concatenation, no JSON.stringify, no object key.

The LRU cache is a standard doubly-linked list with a Map<number, Node>:

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.map = new Map();
    this.head = { prev: null, next: null };
    this.tail = { prev: null, next: null };
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  get(key) {
    const node = this.map.get(key);
    if (!node) return null;
    this._moveToFront(node);
    return node.value;
  }

  put(key, value) {
    if (this.map.has(key)) {
      const node = this.map.get(key);
      node.value = value;
      this._moveToFront(node);
      return;
    }
    if (this.map.size >= this.capacity) {
      const lru = this.tail.prev;
      this._remove(lru);
      this.map.delete(lru.key);
    }
    const node = { key, value, prev: null, next: null };
    this._addToFront(node);
    this.map.set(key, node);
  }
}

Cache hit returns the previous layout result — an object pointing into the arena’s Structure of Arrays (SoA) buffers. The hot path is: hash → Map.get → pointer swap → return. Measured at ~100ns.

Handling Unicode line break rules

The binary search finds where width overflows, but you can’t break in the middle of a word. Unicode Standard Annex #14 defines line break classes for every codepoint.

A DFA classifies each codepoint:

const BreakClass = { SP: 0, BA: 1, HY: 2, ZW: 3, BK: 4, AL: 5 /* ... */ };

function getBreakClass(cp) {
  if (cp === 0x0020) return BreakClass.SP;    // space
  if (cp === 0x002D) return BreakClass.HY;    // hyphen
  if (cp === 0x200B) return BreakClass.ZW;    // zero-width space
  if (cp === 0x000A) return BreakClass.BK;    // newline
  // ... ranges for CJK, punctuation, etc.
  return BreakClass.AL;                        // alphabetic (default)
}

After the binary search finds the overflow point, scan backwards to the nearest valid break:

let breakPos = overflowIndex;
while (breakPos > lineStart) {
  const cls = getBreakClass(text[breakPos]);
  if (cls === SP || cls === BA || cls === HY || cls === ZW) {
    break;
  }
  breakPos--;
}

This backward scan is bounded by average word length (~5 characters in English), making it effectively O(1). For CJK text, every character is a valid break point, so it terminates immediately.

Forced line breaks

Before width-based breaking, scan for n, r, or rn:

let forcedBreak = -1;
for (let i = start; i < len; i++) {
  if (text[i] === 0x000A || text[i] === 0x000D) {
    forcedBreak = i;
    break;
  }
}

If a newline appears within the available width, break there. If width overflows before reaching the newline, do normal width-based wrapping. Newline characters get zero width in the prefix sum so they don’t affect measurements.

CR+LF pairs are consumed as a single break:

if (text[forcedBreak] === 0x000D && text[forcedBreak + 1] === 0x000A) {
  start = forcedBreak + 2; // skip both
}

Putting it all together

The full layout function:

function layoutText(text, containerWidth, glyphTable) {
  const len = text.length;
  const prefixSum = getPrefixSum(len);

  // 1. Build prefix sums (O(n), zero-alloc)
  let cum = 0;
  for (let i = 0; i < len; i++) {
    cum += getWidth(glyphTable, text[i]);
    prefixSum[i] = cum;
  }

  // 2. Find line breaks
  const lines = [];
  let start = 0;
  while (start < len) {
    // Check for forced breaks first
    let forced = scanForNewline(text, start, len);

    // Binary search for width-based break (O(log n))
    let end = findBreakPoint(prefixSum, start, containerWidth);

    // Resolve: use whichever comes first
    if (forced >= 0 && forced <= end) {
      end = forced;
    } else {
      // Scan back for word boundary (O(1) amortized)
      end = findWordBreak(text, start, end);
    }

    lines.push({ start, end });
    start = end + 1;
  }

  return lines;
}

Results

On a typical paragraph (~200 characters, ~10 lines):

Metric Value
Cold layout ~5.6μs
Hot layout (cached) ~100ns
Bundle size ~5KB minzipped
GC pauses 0

The prefix-sum technique isn’t new — it appears in database query engines, computational geometry, and competitive programming. But applying it to text layout, combined with arena allocation and numeric-keyed caching, produces something measurably faster than the scan-and-accumulate approach that most implementations use.

You can try an interactive demo that runs each algorithm step in the browser, or read the source.

Erbil Valisi Hoşnav: “20'den fazla İHA düşürüldü”

Erbil Valisi Ümid Hoşnav, şehirde dün akşam düzenlenen saldırılarda 20’den fazla insansız hava aracının imha edildiğini açıkladı. Olayda can kaybı olmaması sevindirirken, bir kamikaze İHA’nın bir eve isabet ettiği ve yangın çıkmasına sebep olduğu belirtildi.