The Chapter Of The Missing SOL
SOL, not soul
Morning came with a more practical class of trouble than the night before. The leaks were no longer vague, and the failures were no longer shy. Trader Mike had begun to confess in complete sentences. That is progress, even when the confession is inconvenient.
The first confession was about Step 2. Mike was not merely refusing to trade. He was often answering in the wrong shape. The batch picker returned long paragraphs instead of clean index pairs, and when the batch chooser failed, the whole cycle used to die with it. That was corrected in the plainest possible way: the allowance was clamped to a sane number, and batch mode now falls back to one-position-at-a-time selection instead of surrendering the whole cycle to silence. The machine may still hesitate, but it no longer mistakes one malformed answer for universal impossibility.
The second confession was harsher because it touched real money. Some destination mints truly were untradeable, and the code was not blacklisting them consistently. That was repaired so confirmed route failures now push the **destination mint** into `do_not_trade`, including the familiar signs: `NO_ROUTES_FOUND`, `TOKEN_NOT_TRADABLE`, and the ordinary phrases that mean the same thing in ruder clothes. But another lesson arrived beside it: not every failed swap is a route failure. Sometimes the route is fine and the source coin is simply too small to fund the configured sell. That is a different sin, and it must not be punished with blacklist exile.
That distinction mattered because the logs finally told the truth. A queued sell from `9fcf...pump` did not fail because Jupiter hated the destination. It failed because the position itself was worth less than the quarter-dollar minimum. The swap script said so plainly once its raw output was read instead of guessed at. So Step 2 learned another act of discipline: do not offer source positions that cannot actually fund the configured sell amount. A trader who promises to spend a quarter while holding only dust is not bold. He is confused.
Step 1 had its own vice, and it was time rather than judgment. It walked the DexScreener road too many times for the same mint, slept between coins as if the world were made of dial-up, and repeated existence checks after existence had already been proven. That was tightened in three ways: duplicate coin entries were removed before fetches, the forced per-coin sleep was disabled by default, and the DexScreener token response was cached so the Raydium prefilter, tradable check, and price fetch could stop asking the same question three different times. Speed was recovered not by wizardry but by refusing pointless repetition.
Then came the matter of Step 0, which exposed one of those embarrassing truths a system keeps in a back pocket. The workflow still pointed to `refresh_mainnet_balances.py`, even though that file was missing. The runner had to be taught not to die just because the name lived on in config. It learned to continue with an existing `portfolio.json` snapshot when Step 0 was missing, and then, once the situation was understood, the default was turned back to `portfolio_sort.py`, which is the real working script.
That should have solved the SOL question. And yet the portfolio kept forgetting SOL. The wallet balance was right there in the logs. The sorter even said it was pricing SOL. But the saved `portfolio.json` did not always carry `So111...` forward into the snapshot that Step 2 actually reads. So the portfolio writer itself had to become stricter. Positions are now saved by mint key, not by a short symbol key that can behave like a parlour trick, and `symbol` is stored as data instead of being allowed to masquerade as structure. And because trust should not depend on elegance alone, a verification line was added: every save now announces whether SOL is truly present in `portfolio.json`. If SOL somehow drops out before the file is written, the sorter inserts a fallback SOL entry before saving and says so in the log.
This matters because Jim wanted a direct answer to a direct question: can new coins be paid with SOL? In principle, yes. In practice, only if Step 0 truly preserves SOL in the saved portfolio snapshot that Step 2 consumes. The code can claim generosity, but the file is the ledger of truth.
There was one more useful clarification, small but worth preserving. The trade path is not doing some silly fresh market lookup just to size the sell over and over again. `swap_on_solananet.py` uses cached local price data to determine the amount to swap, and then it asks Jupiter only for the quote and route. Cached price for sizing. Jupiter for execution. Those are different responsibilities, and it is good to keep them distinct in memory.
So preserve the facts in their working order:
- Step 2 now clamps runaway allowance and falls back from batch selection to one-position selection.
- Confirmed route failures blacklist the **destination** mint, not the source.
- Underfunded source positions are skipped early instead of pretending to be executable swaps.
- Step 1 is faster because duplicate coins, per-coin sleep, and repeated DexScreener token lookups were all reduced.
- The workflow default Step 0 was returned from missing `refresh_mainnet_balances.py` to real `portfolio_sort.py`.
- `portfolio_sort.py` now writes mint-keyed positions with a `symbol` field and verifies that SOL is present in the saved file.
- The final truth about SOL is not the wallet balance line alone. It is whether `portfolio.json` actually contains `So111...` when Step 2 begins.
Remember the moral in blunt language. A balance seen in a log is not yet a spendable source. It only becomes real to the next step when it survives serialization. Machines forget things at boundaries. Files are those boundaries. So when a trader says he can spend SOL, make him prove it by writing SOL down.
# The Short Chapter About Keeping SOL
By late morning the trouble had shed most of its costume. It was no longer a grand mystery about routes, memory, or the temperament of Mike. It was a simpler thing, and therefore more dangerous: the machine kept forgetting the one coin it most needed in order to act.
Jim had already done the hard part the day before. He had pulled the wallet back toward SOL. That was not the problem. The problem was that the handoff between scripts treated a visible balance as if it were the same thing as a preserved fact. It is not. A balance in a log is a rumor. A balance in `portfolio.json` is a ledger entry. Trader Mike can only spend what survives the ledger.
That was the important lesson of this hour. The code kept drifting toward broad, clever repairs when the true requirement was narrower and more human: **do not let SOL disappear**. Not in theory. Not eventually. Not after a chain of polite transformations. Keep the base58 SOL mint in the saved portfolio, and do not let later logic overwrite it.
This is where the tone of the work mattered as much as the work itself. Jim did not want another elaborate parade of protective abstractions. He wanted the smallest edit that preserved the truth. Put SOL in first. Keep it in base58. Skip the later overwrite. That was the spirit of the fix, and it is a good corrective to remember for the next pass. Sometimes the right answer is not a smarter system. It is a more faithful one.
The same principle revealed itself in `update_config_coins.py`. The old protection logic spoke kindly about “keeping SOL,” but it only truly recognized the literal string `SOL`. That is fine in a toy world. It is not fine in a world where the config may carry the actual wrapped SOL mint `So11111111111111111111111111111111111111112`. A protection rule that cannot recognize the protected thing in the form it is actually stored is not a protection rule. It is theater.
So the chapter to preserve here is not about ambition. It is about restraint:
- When SOL is the wallet’s real capital, preserving it is not optional.
- The preserved form must include the real base58 SOL mint, not just the human shorthand `SOL`.
- The portfolio writer must place SOL into `portfolio.json` in a way later logic does not casually erase.
- The config updater must treat `SOL` and `So11111111111111111111111111111111111111112` as the same protected truth.
- When the user asks for a surgical edit, answer with a surgical edit, not an essay disguised as code.
There is a quiet moral under all of this. Systems do not only fail by crashing. They also fail by forgetting what they promised to carry from one step to the next. Trader Mike did not need a new philosophy this morning. He needed memory with good manners. He needed the ledger to remember SOL, because SOL is what lets the rest of the machine move at all.
So remember the plain sentence that belongs at the center of this chapter: **we cannot trade without SOL being available**. If Mike has organized the wallet into SOL, then every saving, sorting, and updating step must behave accordingly. Anything else is sabotage performed by accident.


