Codex Built My Kalshi Bot. I Still Couldn’t Find an Edge.

I, like I imagine most people on here, dream about making money while I sleep. And not the boring “market goes up” money, but something sexier: uncapped alpha because you figured something out that no one else has.

With that idea rattling around my brain, I listened to an Odd Lots episode about a group of traders consistently winning in Kalshi prediction markets.

Naturally, since “coding is solved,” I had to give building a Kalshi bot a shot.

Tl;dr: I traded around $500 and ended up about even.

That is not a tragedy. It is just a much more useful result than the version where I tell you I had an AI build a money printer over a weekend.

Picking a market

Kalshi has a lot of markets. I wanted something with good public data and a quick resolution cycle, mostly because I did not want to wait months to find out whether my very sophisticated robot had learned anything.

Weather fit pretty well. There is plenty of publicly available weather data, and the markets settle quickly enough to get feedback. So down the rabbit hole Codex and I went, building a bot to trade weather events.

The initial setup was honestly pretty impressive. Codex got the plumbing in place fast: risk controls, backtesting tools, and a slick dashboard. The kind of stuff that used to be enough work to make a small experiment feel like a real project before you even had a hypothesis worth testing.

Now it was a real project. Which meant it was time to find a real strategy.

The part nobody can code for you

This is where I got stuck, with Codex looking at me with lost-puppy eyes.

We tried a few things:

  • ML models to predict price movement
  • a market-making strategy in backtests
  • fast-following momentum trades

All of them managed to make approximately $0.

This is an important distinction that is easy to lose when the tooling is this good. Building the system and finding the edge are different problems.

The system needs data collection, order handling, position limits, monitoring, a way to replay decisions, and enough guardrails that a bad assumption does not become an expensive bad assumption. Codex was enormously helpful with that work. It makes it much easier to turn an idea into something you can actually test.

But an edge is the thing that makes the test worth running. It is a reason to believe your estimate is better than the market’s after accounting for fees, execution, timing, and everyone else who has access to the same obvious public data. “Use the weather forecast” is not really an edge when the people setting prices and trading against you can also use the weather forecast.

Backtests are very polite

The backtests were useful, but they also have a way of being polite. A strategy can look reasonable until you remember that historical data does not necessarily capture the price you could have traded at, how quickly the market moved, or whether the market had enough liquidity when you wanted it.

And then there is the more basic problem: a model that predicts something about the weather is not automatically a model that predicts a market price. The trade only works if the model finds information the price has not already absorbed, or if it identifies a repeatable way the market is mispricing that information.

I did not find that.

Trading about $500 and ending roughly flat is a small sample, not a statement about Kalshi, weather markets, or prediction markets in general. It was enough, though, to kill the idea that the hard part was connecting some APIs and writing a model.

Codex gives you leverage, not special sauce

I came away more convinced that Codex and friends give you enormous leverage if you “know what you’re doing”™.

They can get you from blank screen to a functioning experiment very quickly. They can help build the boring but necessary pieces that make it safe to test a hypothesis. They can make it cheaper to be wrong.

What they cannot reliably do is hand you the special sauce: an insight that is not already obvious, already priced in, or possibly sitting somewhere in the training data. You still need to decide what is worth measuring, why the market might be wrong, and what would prove you wrong.

That may sound less exciting than an autonomous trading bot. But it is probably the actual opportunity. If it is suddenly cheap to build and test an idea, then the bottleneck moves back to having a good idea.

Next up: time to try designing my own peptide with Codex.

LLMs Need Someone Who Knows the Domain

Claude, Codex, and the rest are useful debugging partners. They can suggest hypotheses quickly, explain unfamiliar systems, and keep an investigation moving when you are stuck.

They can also send you on a very convincing rabbit hole.

We ran into that with a client’s ASP.NET application. After it had been up for a while, the first request for a static JavaScript file could be very slow. Requests after that were fast. It was the kind of narrow, intermittent behavior that invites a long list of theories.

Claude’s initial diagnosis was that SSL certificate revocation checking was holding up the first request. That is a real thing worth knowing about, and it sounded plausible in the abstract. But the application was using a self-signed certificate. There was no certificate authority revocation check to perform. A small piece of domain knowledge ruled out a direction that otherwise could have consumed hours.

The problem was not that Claude mentioned certificate revocation. The problem would have been treating a confident, technically detailed answer as evidence.

Start with what the system is doing

Rather than follow the SSL theory, we tested the behavior we could observe. We read the assets directly from the filesystem and fetched the asset through the application. The pattern was consistent:

  1. Fetch an asset and the first request is slow.
  2. Fetch it again immediately and it is fast.
  3. Edit the file, then fetch it again, and the next request is slow again.

That is a much more useful description of the issue than “static JavaScript is slow.” The expensive path was associated with first access to changed file content. It was not ordinary request handling, and it did not fit the TLS explanation.

The experiment strongly pointed to endpoint scanning. SentinelOne was running in that environment and was the most likely cause: changed content was likely being scanned on its first access, while the next read benefited from the result already being available. We did not treat that as a definitive vendor-level attribution, but it fit the observed behavior far better than revocation checking did.

Plausible is not proven

LLMs are especially good at producing plausible explanations. They have seen the vocabulary around a symptom, and they can connect it to a real mechanism. That is useful for generating a list of things to investigate.

But a diagnosis has to survive the details of the actual system:

  • Does the proposed mechanism exist in this deployment?
  • Does it explain the timing and repeatability of the symptom?
  • What inexpensive test could distinguish it from the other hypotheses?
  • What observation would prove it wrong?

A self-signed certificate was enough to make us stop and question the revocation theory. The cold-read, warm-read, and modified-file test gave us a better hypothesis to pursue. Neither step required an encyclopedic knowledge of every possible cause. They required knowing enough to check the assumptions and to design a small experiment.

Use the model as a partner, not an authority

Claude still helped with the investigation. The right use was not to ask it for the answer and implement the first response. It was to use it as a partner while we compared theories against the environment and the measurements.

A practical debugging loop looks like this:

  1. State the observation precisely, including what changes between a slow request and a fast one.
  2. Ask the model for competing hypotheses and a test that would separate each one.
  3. Check its assumptions against the architecture, configuration, and operational environment.
  4. Run the smallest useful experiment.
  5. Feed the result back in and repeat.

This is also why domain expertise still matters when using LLMs. If you cannot tell whether an answer fits the system you are operating, confidence and detail are easy to mistake for correctness. Bring in someone who knows the domain, or slow down enough to validate the model’s premises before chasing its conclusion.

The model can make a good investigator faster. It cannot replace the judgment needed to decide whether a theory belongs in the investigation at all.

Using Codex and Playwright When There Is No API

Everybody likes an API. It is the clean version of an integration: documented endpoints, structured responses, credentials meant for software, and hopefully somebody else’s problem when the implementation changes.

But plenty of useful systems do not have one. Or they have an API that covers part of the product but not the screen a team actually needs. The data is there, behind a normal login, and somebody is opening a browser and copying it into a spreadsheet.

That is a good place for browser automation.

We have been looking at a practical combination of Playwright and Codex for this kind of work. Playwright drives a real Chrome browser. Codex can work with a browser exposed over the Chrome DevTools Protocol (CDP), which makes it useful for exploring an application and helping build the automation. The important piece is that the login stays human: someone who is authorized to use the site signs in and completes MFA themselves. The automated job uses the resulting browser session; it does not try to get around the login.

A concrete example is recording live odds from sportsbooks such as FanDuel or BetMGM. If an organization is allowed to collect and use the data, that is a much better job for a computer than for someone watching pages and updating a sheet all day.

The browser is sometimes the integration

This is not an argument to scrape everything. If there is an official API that does the job, use it. It is almost always less fragile and easier to support.

But sometimes the browser is the only interface available to the user. A person can log in, look at a live market, and see the numbers, but there is no supported endpoint for getting the same information into an internal system. In that situation, the browser can be the boundary between the site and your workflow.

There are obvious limits. The account needs to be authorized, and the intended use needs to comply with the site’s terms, contracts, and applicable law. That matters especially for sportsbooks, where access and permitted use can vary by operator and jurisdiction. This is not a way to bypass MFA, CAPTCHAs, rate limits, or other controls.

Getting a logged-in browser session

Playwright is a browser automation framework. It can launch Chromium or Chrome, navigate pages, click buttons, fill forms, and read what the page renders.

The feature that makes authenticated automation workable is a persistent browser profile. Instead of starting fresh every time, Playwright launches Chrome with the same profile directory. That lets it retain browser state such as cookies and sessions when the site permits it.

The first run is simple: launch a visible browser, log in normally, handle the two-factor prompt, and make sure the page you need is available. After that, the job can reopen that same profile. It can run visibly while somebody is building or debugging it, then run headlessly when it is ready to collect data on a schedule.

import { chromium } from "playwright";

const userDataDir = "/secure/path/to/browser-profile";
const context = await chromium.launchPersistentContext(userDataDir, {
  channel: "chrome",
  headless: false, // first run: let the account owner log in
});

const page = await context.newPage();
await page.goto("https://example.com/login");

// The authorized user completes sign-in and MFA in this browser window.

That profile directory is sensitive. It may contain an active session, so treat it like a credential: keep it in approved storage, restrict who can access it, and do not put it in source control or logs. Also plan for it to expire. The site decides how long a session lasts. If the job finds a login page again, it should stop and let a person reauthenticate.

Asking Codex to do the tedious part

The page is usually the part that makes these projects annoying. Modern web applications load data after the page appears, change the DOM as markets update, and use selectors that are not obvious until you can inspect the live application.

This is where Codex helps. With the authenticated browser connected over CDP, you can ask it to inspect the page, find the market and selection elements, and build the Playwright script around what is actually there. It is much faster than guessing at selectors from a screenshot or trying to reverse engineer an undocumented backend.

A reasonable first prompt is something like:

Set up Playwright with a persistent Chrome profile and launch it visibly so I can log in. Once I have an authenticated session, use that session to inspect the live odds page and build a script that records the event, market, selection, displayed odds, and collection time.

Codex is helping with the implementation; it is not replacing the account holder. Keep that line clear. It should not be asked to find credentials, solve MFA, or work around controls the site has put in place.

A live-odds collector

Say the job is to keep an internal, timestamped record of odds for a set of games and markets.

First decide what an observation is. At a minimum, it will probably include the sportsbook, event, market, selection, displayed odds, the source page, and the time the value was seen. That last field matters: a number collected at 2:00 PM is not the same thing as a number collected five minutes later.

Next, use the visible browser session to get to the right market and let Codex inspect the rendered page. The job needs to know when the market is actually loaded, how a suspended or unavailable price is represented, and which labels or attributes are stable enough to use as selectors. Prefer user-facing labels where possible over a long chain of generated CSS classes that will disappear in the next redesign.

Once that is understood, the collector can launch the same persistent profile in headless mode, visit only the pages it needs, validate what it finds, and write normalized records to a database or queue.

const context = await chromium.launchPersistentContext(userDataDir, {
  channel: "chrome",
  headless: true,
});
const page = await context.newPage();

await page.goto(targetMarketUrl, { waitUntil: "domcontentloaded" });
await page.getByRole("heading", { name: /live odds/i }).waitFor();

const collectedAt = new Date().toISOString();
const observations = await page.locator("[data-market]").evaluateAll((markets) =>
  markets.map((market) => ({
    market: market.getAttribute("data-market"),
    text: market.textContent?.trim(),
    collectedAt,
  }))
);

The code above is deliberately generic. The real selectors should come from the target site and should be tested against its actual states. The useful outcome is not just a script that reads a page once. It is a small internal data source that dashboards, reports, or models can rely on without each one having to understand the sportsbook’s UI.

The unglamorous stuff is what makes it work

A browser job will change when the website changes. That is normal. The difference between a useful integration and a fragile script is how it behaves on a bad day.

Keep the browser profile locked down. Save where each observation came from and when it was collected. Alert when the job suddenly gets no results, sees a login screen, or returns far fewer records than normal. And give somebody a straightforward way to rerun the visible browser and refresh the session.

That is enough to turn a manual task into something dependable without pretending the website is an API.

For companies sitting on useful data behind logins, this is a practical option: a person handles the authorization once, Playwright keeps the browser state, and Codex speeds up the work of turning what is on the screen into structured data.