Side Projects

I Called It Dead on Friday. It Started Working on Saturday.

September 2026 · 7 min read

Fifteen days. Three Client IDs. Four OAuth consents. A signed DocuSign agreement. An "access is now live" email that did nothing. I gave up two days before the draft. Yahoo propagated overnight.

I wrote it off on a Friday.

Fifteen days. Three Client IDs. Four full OAuth consents. A signed DocuSign agreement. A support thread. An email from fantasyapideveloper@yahoosports.com at 8:19 PM with the subject "Your Yahoo Fantasy Sports API access is now live." That email did nothing. I tested for an hour, updated the status doc, and went to bed.

The draft was Sunday. It was Friday. Manual entry was already built and tested against 160 lots of real data. The API would've let me skip typing player names as bids came in — a convenience. Not essential. I wrote it off.

I tested it again Saturday morning, on a whim.

It worked.


What the Tool Is

A local co-pilot for a live fantasy auction. Ten managers, $200 each, 16 roster spots, 160 lots, superflex. It doesn't bid and it doesn't touch the draft room's UI. It sits in a browser tab next to it and answers one question continuously: what is this player worth to me right now, and who can outbid me for him.

Three tokens per lot — jeanty 31 me — and it reprints the full board. Python standard library, one optional dependency for fuzzy name matching, no framework, no build step. Speed matters — lots close in ten seconds.

There were three things it needed to get right, and one of them was backwards in the original spec.

Auction inflation: overspending early makes prices fall, not rise. The dollars that would've chased everyone still on the board are gone. The original plan had this reversed. Replaying the 2025 draft settled it empirically — inflation ran 1.00 to 0.46 by lot 100. Real bargains. The spec would have told me to skip them.

Opponent max bid is arithmetic, not psychology. (200 − spent) − (16 − filled − 1). Every manager must leave a dollar per empty spot, so their true ceiling is exact. When cF is at $22, I stop worrying about cF. No modelling.

The scarcity signal had to earn the right to interrupt. First version fired BID NOW from lot one — because a two-player tier against ten managers really is nearly gone, from the start. The fix was anchoring demand on starters, not roster headroom. The flag means something by the time it fires. Most of the work in a tool like this is deciding what not to say.


The API Fight

Yahoo approved the developer application August 23rd. Fifteen days of 401 additional_authorization_required followed.

The diagnosis took a week to nail. Two 401s that look identical are not the same failure.

401 additional_authorization_required — the app lacks the Fantasy Sports scope. Turned away at the door. Permanent. The original application was created before the permission existed in the console, so it could never pass regardless of how many times I re-consented.

403 not authorized to perform this action — the app has the scope, cleared the OAuth check, and is waiting on Yahoo's application allow-list. Further along than a 401. Only Yahoo can lift it, and only against the specific Client ID they've reviewed.

I read the 403 as worse and lost a day. That was the expensive mistake.

What broke it open was keeping the dead app as a control. Testing one credential set proves nothing. Testing two — where one is known-dead — tells you which failure you're looking at.

The dead app returned 401. The new one returned 403. That contrast was the diagnosis. Right app, right scope, blocked by Yahoo's backend. Nothing local could move it.

Then the "access is now live" notice arrived. I tested for an hour. Still 403 on every endpoint. I wrote it off.


Saturday

[403] openid userinfo           ← still harmless (OIDC perms never ticked)
[200] /fantasy/v2/game/nfl
[200] /users;use_login=1/games
[200] .../games;game_keys=nfl/leagues

League key: 461.l.269312. "League PoopyBubbles." The only league on the account.

160 lots pulled clean against the test fixture. Every price, every buyer, $1,941. Yahoo had propagated overnight — roughly 28 hours after the notice that did nothing.

The honest lesson isn't "never give up." Every technical call in those fifteen days was correct. The right app, the scope attached, the block on Yahoo's side, nothing local to move it. The only wrong call was treating a lack of progress as a conclusion.

When you're blocked on infrastructure you don't control, re-test on a schedule instead of concluding.


A Few Bugs Worth Keeping

There was a float that only appears when you're losing. An adjusted price shipped unrounded. At lot one, inflation is exactly 1.00, so every number lands on a whole dollar and it looks perfect. It surfaces as $34.90618101545254 mid-draft, once the market moves. Not the ideal moment to find it.

There was a Windows double-click bug. On Windows, HTTPServer sets allow_reuse_address, which — unlike Linux — lets a second process bind a port one is already listening on. No error. Full success banner. Only one is actually serving, while the other holds a frozen copy of your draft state from the moment it launched, pointed at the same file. Close the first window and it overwrites everything. Found because a double-click launcher had just made that mistake easy to make.

There was a test that caught its own vacuity. Written to prove no floats leaked into money fields, its first fixture sold every player at book value — which holds inflation at exactly par by construction, so it tested nothing at all. It now asserts the market moved before it checks anything.

287 tests. Full-draft replay of all 160 lots of 2025 against the live API. The tool has never seen a live auction.

Draft is tomorrow.

That's the honest ending, and it's the right one.