< Back to Blog

The Difference Between a Side Project and a Product (I Learned It the Hard Way)

TL;DR / Key Takeaways

  • A side project only has to work for you. A product has to work for someone who does not think like you, does not have your environment, and cannot read your mind.
  • The transition happens at a specific moment, not gradually. For me it was an 11pm email from a buyer who could not get the bot running.
  • Documentation, error messages, and onboarding are not nice-to-haves. They are the product. The code is almost secondary.
  • The Predict & Profit bot went through this transition in public. The v2.1 codebase exists partly because of what that first buyer email forced me to fix.

I spent about six months building the bot for myself. It ran on my VPS. It traded my money. When something broke, I fixed it, because I knew exactly where to look. When the log output was cryptic, that was fine, because I wrote it and I knew what it meant.

That is a side project. It works because you are the only user.

The first sale changed nothing technically. Same code, same VPS, same bot. But then the email came.

It was 11pm on a Tuesday. A buyer, somewhere, had downloaded the source and hit a wall during setup. The error message they were staring at said something like KeyError: 'market_positions'. They had no idea what that meant. I knew exactly what it meant, because I had seen it before and just worked around it in my head. It never occurred to me to make it clearer, because why would I? I already knew.

That was the moment. Not the first sale. The first support email.


What Side Projects Are Actually Like

Side projects are personal. The code is written for one brain, yours, in one environment, yours, with one set of assumptions, yours.

My bot had a config file with six API keys. The comments said things like "put your key here." There was no explanation of which Kalshi permissions the key needed, no note about RSA key format, no warning that the FRED API key takes a few minutes to activate after registration. I knew all of that. It lived in my head.

The SETUP_KEYS.md file did not exist. There was no 7-day deployment guide. Error handling was whatever Python threw by default, which is often a stack trace that means nothing to someone who did not write the code.

None of that mattered when I was the only user. It all mattered the second someone else tried to run it.


The Specific Bug That Forced the Transition

The KeyError: 'market_positions' error the buyer hit was a real bug I had never properly fixed. The Kalshi API returns a response with two keys: event_positions and market_positions. My get_positions() wrapper was reading the wrong one, so it returned an empty list every time.

When I ran the bot myself, I had muscle memory for when positions looked off. I would check the Kalshi dashboard manually. I had workarounds I did not even realize were workarounds.

The buyer had none of that context. They just saw an error and a bot that would not start.

Here is the relevant fix, which should have been there from day one:

def get_positions(self) -> list:
    """
    Fetch all open positions from Kalshi.
    Returns list of market_positions dicts.
    """
    url = f"{self.base_url}/portfolio/positions"
    response = self._get(url)

    # Kalshi returns {'event_positions': [...], 'market_positions': [...]}
    # We want market_positions for individual contract data.
    # event_positions is the rolled-up view per event, not per contract.
    positions = response.get("market_positions", [])

    if not positions:
        logger.debug("get_positions: returned empty list (no open positions or key mismatch)")

    return positions

One line of code. One correct key name. The bug had been there the whole time. I just never noticed because I had unconsciously built a mental workaround.

That is what side projects do to you. They let you be sloppy in ways that only matter when someone else shows up.


Documentation Is Not the README

I thought I had documentation. I had a README with installation instructions and a list of command-line flags. That is not documentation. That is a man page.

Documentation for a product means anticipating where someone who has never seen your code will get stuck. It means writing the thing you say in your head when you explain the setup to an imaginary person.

After that first email, I wrote SETUP_KEYS.md. It covers:

  • Where to create a Kalshi account and which API permission level to select
  • How to generate an RSA key pair in the format the bot expects
  • Where to put the keys so the config loader finds them
  • What each FRED and BEA API key is for and how long activation takes
  • What the first dry-run log output should look like, so the buyer knows it is working

That last one matters more than people think. If you have never run a trading bot before, seeing a log full of SKIP reason=min_confidence might look like failure. It is not, that is the filter working correctly. But if nobody tells you that, you assume something is broken and you email the developer at 11pm.


Error Messages Are Part of the Product

Default Python errors are written for developers who know the codebase. AttributeError: 'NoneType' object has no attribute 'values' means something to me. It means nothing to someone who just wants the bot to run.

After the transition I started treating error messages as user-facing copy. Not all of them. But the ones that fire during setup and first run, the ones a new buyer is most likely to hit.

try:
    ensemble_data = self.fetch_aigefs_ensemble()
except S3FetchError as e:
    logger.error(
        "AIGEFS fetch failed. This usually means the S3 bucket is temporarily unavailable "
        "or the date prefix is wrong. Check that your system clock is correct and retry. "
        f"Raw error: {e}"
    )
    return None

The raw error is still there. But now there is context. Now there is a next step. That is the difference between an error message written for yourself and one written for a customer.


The Responsibility Shift Is Not Optional

When money changes hands, something changes. The buyer is not obligated to figure out your mental model. They paid for something that works, and if it does not work out of the box, that is your problem, not theirs.

This is not a complaint. It is just true.

I have had buyers email about Windows path issues I never encountered because I develop on Ubuntu. I have had questions about Python version compatibility I did not document because I assumed everyone was on 3.12. I have had someone ask what "dry-run mode" means, which I thought was self-explanatory and apparently is not if you have never run an algorithmic trading bot before.

Every one of those questions is a documentation failure, not a user failure.

The side project version of the bot was built to satisfy my own curiosity and trade my own account. The product version has to work for a data engineer in Toronto who has never touched GRIB2 files, and a developer in Austin who knows Python but has never used the Kalshi API, and someone in Manila who is running this on a Windows machine I have never tested on.

That is a different engineering problem than the one I thought I was solving.


What Actually Changed in v2.1

A lot of the v2.1 changes get described as feature additions. The AIGEFS integration, the 4-source ensemble, the early-exit logic. Those are real features.

But a significant chunk of the work was fixing the gap between "works for me" and "works for someone else."

The trade_decisions table now logs every rejected trade with the skip reason, not just the trades that fired. That was useless to me personally. I could read the log. But a buyer trying to understand why the bot is not trading needs to see the full decision trail, or they assume it is broken.

The settlement status bug, where settled trades stayed marked as open in the local database, never affected my live trading because I checked the Kalshi dashboard directly. A buyer running in dry-run mode, trying to learn the system, would see a database full of trades marked as open that had already settled days ago. That looks like a serious bug. It is a serious bug. It just never bit me.

Fixing it was not a feature. It was a basic obligation to the people who paid for the code.


The Bot Is Still a Side Project in One Way

I am still the only support channel. There is no team. There is no ticket system. There is just my email address and a Gumroad product page.

That is fine for now. But it means every question I get is feedback on something I failed to document or anticipate. I try to treat it that way. When the same question comes in twice, that is a documentation gap, and I update SETUP_KEYS.md or the deployment guide before it comes in a third time.

The bot is at $97 with 12 sales and zero paid ads. That is a small number. But those 12 people paid real money for something I built, and they deserve code and docs that actually work.

That is the obligation. That is what the product is.


Building something for yourself is satisfying in a specific way that building for others is not, and vice versa. The side project phase was where I figured out if the idea was real. The product phase is where I find out if the execution is good enough for someone who does not share my brain. I am still finding out.