All or Nothing: Why We Left Edward Jones

In May of this year my wife and I moved all four of our IRAs from Edward Jones to Fidelity. We did not plan to do that. I had called our advisor to tell him about one rollover, a single account, and by the end of the conversation we had been told it was all or nothing. We were on vacation in Florida at the time, so a good part of that week was spent on the phone coordinating transfers instead of sitting by the water. I have had a few months to think about it now, and I want to write down what happened, what it cost us, and what I would tell someone who is in the same spot we were in.

I am going to name Edward Jones because that is where our accounts were held and where this happened. The requirements described here are what our advisor and the firm told us during the process. I am not going to name our advisor. He was more than an advisor to us. He is a brother in Christ, someone we met through church, someone my family likes and still likes. I believe he did what he thought was right, and he may well have given me the same advice he would give his own family. None of that changes the facts, and I have tried to keep this to the facts.

Some context first. Edward Jones never had all of our money, and I am glad of that now. My 401(k) is through my employer and sits at Fidelity, where I pick the funds myself. My company stock is fully vested and held at Principal. Neither of those could be managed by Edward Jones. They could factor them into a plan, but they could not touch them. What they did manage was my traditional IRA, my Roth IRA, and my wife's traditional and Roth IRAs. All together that was roughly 40 percent of what we have.

We knew there were fees. I want to be fair about that. Nobody hid the fact that we were paying for the service. What was never clear was how much. If you looked at a monthly, quarterly, or annual statement, you could not find a single number that told you what you had paid that period and what it was for. There was an advisory fee. There was a program fee. There were fund expenses inside the investments themselves. The number you heard in conversation was around 1 percent. When we finally sat down and added it all up across all four accounts, we estimated the all-in cost at more than $10,000 a year, including the expenses built into the funds. For some holdings, fund expenses and account-level charges together put the all-in cost close to 2 percent.

For that, we had one meeting a year.

The thing that got me started was trying to figure out how to do a backdoor Roth conversion. Edward Jones had suggested opening additional traditional IRAs, but they did not seem sure how to work through it. I held off. Through my own reading, and through conversations with people who were not at Edward Jones, I learned that I could roll eligible pre-tax money from my traditional IRA into my employer 401(k) without triggering income tax or an early-withdrawal penalty. That would let me empty the existing IRA before doing the conversion. Transfer fees are a separate question. A quick call to Fidelity confirmed that our plan accepts incoming rollovers. The 401(k) had a fund that was very close to what the IRA was already holding, and the expense ratio on it was 0.08 percent. That is not a typo. On an account of a little over $50,000, I was paying close to $1,000 a year in fees at Edward Jones. The fund expenses on the same money in the 401(k) would be about $40 a year, before any separate plan charges.

So I called our advisor. I had not decided anything yet about the other three accounts. I only wanted to tell him I was going to move this one, and I was hoping for an objective conversation. What I got was this, almost word for word: any time I have clients who are worried about fees, it is because they are running short on money and looking for more to spend.

I had just finished my annual review with him. I had spent that meeting explaining that I wanted to retire early and was looking for every lever I could pull to get there. He knew I was not looking for spending money. He also knew, or should have, why I wanted to compare the IRA's roughly 1.8 percent all-in cost with the 401(k) fund's 0.08 percent expense ratio and any separate plan charges. I wanted to understand what the extra cost was buying us. He told me he did not recommend it and suggested we revisit in six months. I said no, I am moving this one, and we can talk about the others later.

That is when it became all or nothing. He told me that if I moved that account, I had to move the other three IRAs too. His explanation was that Edward Jones wants clients above a certain household balance in order to justify the cost of serving them, that we were below that line, and that we had been below it for a while. As he put it, if more money left, there was no reason to keep us.

I had been a client for a long time. Until that call I had never been told about such a threshold. If we had been below it for years, I would have liked to know that years ago, when I could have decided for myself whether to add money, move money, or leave. Instead it was presented as though they had been doing us a favor by keeping us, which was hard to square with the $10,000 a year we had been paying for the favor.

What I understand least is the timing. They knew about the 401(k) and the company stock. Those accounts were in every plan they ever built for us, and they knew that at retirement all of it would roll somewhere. Edward Jones was the obvious somewhere. All they had to do was wait. Instead, over a $50,000 rollover, they let a long-time client walk, along with the other three IRAs and everything they stood to manage later.

Since we were moving all four IRAs anyway, I want to say what we were getting for that money, because the pitch was never just about picking funds. The pitch was estate planning, trusts, wills, life insurance, college savings, the whole picture. In practice almost every one of those turned into a referral. Trust planning meant the names of three attorneys to call on our own. Life insurance meant an application to an outside carrier. Some things lived in the Edward Jones system and some did not, and keeping track of which was which was our job. Toward the end we were checking their paperwork ourselves, because forms were coming back with misspellings, and transfers were not landing when they were supposed to.

And then there was the simple matter of getting your own money out. Putting money in was easy. Taking it out could not be done from the website or the app. I had to call, and sometimes sign something, and sometimes that something had to be faxed. When they proposed a new product to me not long before all this, my first question was whether I would be able to withdraw from it online. The answer was no. This was 2026.

The rollover itself deserves its own paragraph, because it is the part I would most want someone else to know ahead of time. Fidelity had confirmed by phone that the 401(k) would accept the money and had given me the account details. I filled out the Edward Jones form. Edward Jones denied it. They wanted proof that the receiving plan accepted rollovers. I sent them the plan document with the page and section highlighted. They denied it again and said they needed a letter, on Fidelity letterhead, stating that the plan accepted the rollover and that it had been approved. I talked to several people at Fidelity, including the representative who manages our company's plan. Every one of them said the same thing: they had never been asked for such a letter, the plan document had always been enough, and this is one of the most routine transfers in the business. Money moves between Edward Jones and Fidelity every day.

We never got the letter. What we did instead was this:

  • Sold everything in the traditional IRA at Edward Jones to cash. The proprietary funds could not have transferred in kind anyway.
  • Transferred the cash broker to broker into a new traditional IRA at Fidelity. No check, no letter, no approval needed.
  • Rolled that Fidelity IRA into the Fidelity 401(k). Same custodian on both sides, so it was handled in house.

It worked. The money landed in the 401(k) as a rollover, in the fund I wanted, with a 0.08 percent expense ratio. It also left the traditional IRA empty, which was what I had been trying to accomplish for the backdoor Roth conversion. For anyone looking into the same thing, the IRS guidance on Form 8606 explains how other IRA balances and year-end values factor into the tax calculation. There is more to it than emptying one account.

The other three IRAs were simpler. Fidelity did most of the work, and the money arrived in the funds we chose. Since May our returns have held steady or done slightly better than they did in the managed accounts.

The 529s and insurance were another matter. We had to close the advisor-sold 529s and open new direct-sold accounts for each of the kids to get the advisor off of those. The insurance took a pile of painful paperwork with multiple companies, both to remove the advisor and to correct beneficiary mistakes Edward Jones had made on several accounts. We had tried to get those mistakes corrected before and never seemed to get them resolved.

I want to be careful about what I am saying here, because I am not against financial advisors. There are seasons of life and sizes of estate where paying someone is clearly the right call. What I am against is a fee structure that cannot be read on a statement, a service that is mostly referrals, and an advisor who treats a question about cost as a character flaw. Ten thousand dollars a year, invested instead of spent on fees, is a few hundred thousand dollars over twenty years. That is not spending money. That is years of retirement.

On retirement, one more thing. Every projection I saw at Edward Jones showed a shortfall if I tried to retire before 60. The answer was always that it could not be done. When I ran the same numbers myself, with the advisory fees removed and a few ordinary changes, the gap got small very quickly. Fewer trips. One fewer vehicle. Helping with the kids' college instead of paying all of it. Taking a bit less each year than we first assumed. None of that is complicated, and none of it ever came up in a review. I just know that 59 looks realistic now, and 55 is not out of the question.

If you are sitting where I was a year ago, here is what I would ask before you do anything else:

  • What is my total all-in cost, in dollars, for the last twelve months? Not the advisory fee. Everything.
  • What did I receive for that, specifically, that I could not have done myself or paid for once?
  • If I needed $5,000 from this account on a Tuesday, how would I get it, and how long would it take?
  • Is there a minimum balance, and am I above it?

If you cannot get clear answers, that is an answer.

Only time will tell whether our plan works out the way the spreadsheets say it will. What I can tell you today is that we know exactly what we own, exactly what it costs, and exactly how to get to it. We sleep better. It is a strange thing to say that being shown the door was a blessing, but that is how it has turned out, and I am grateful for it.

One Month Later: Claude Earned My Work Back

A month ago I published Claude Is Losing My Work to ChatGPT. I was frustrated when I wrote it. I tried to keep the frustration out of the conclusions, and I am no longer sure I fully succeeded. I had left ChatGPT for Claude in March of 2025 and had been open about why. My wife followed me over, and she has become more committed to Anthropic than I am. So it was strange to be the one in our house writing that Claude was losing my work to the tool I had left.

I ended that post by saying Anthropic could earn the work back and OpenAI could lose it. I wanted that to be true. I did not expect it to happen this quickly.

Looking back, two things were going on in September, and I ran them together.

The first was the usage problem, and that was the one driving my frustration. I would sit down to work on the My Family Devotion redesign, get a couple of good rounds in, and the usage meter would tell me I was done for now. My monthly allowance was going the way my kids go through a Costco box of granola bars: fast, and with no idea where it all went. I checked my habits. I looked at how much context I was loading. I read Anthropic's usage documentation twice. I never found the cause.

The second was that on that particular project, ChatGPT, running OpenAI's Astra model, did better work. It gave me a cleaner site with fewer corrections, and Astra noticed that the individual devotion pages had no unique titles or descriptions, which Claude had missed. That result was real, and I would report it the same way today.

What I would not do again is let one project, worked on while I was already irritated about usage, stand in for a verdict on the whole tool. The redesign was front-end work, the kind of thing I do least often, and I was running it on Claude in short bursts because the meter kept cutting me off. I do not know how much of the gap was the model and how much was me never getting a full session with it. I did not ask that question in September. I should have.

October was a heavy month at work, and that turned out to be the test.

A monthly report I had been building by hand for a client became something that builds itself and verifies its own numbers against the source before it writes anything. A security design I had been putting off had more edge cases than I expected, and I worked through all of them. An old console application finally got its keys out of config files and into environment variables. I spent part of an afternoon untangling an Azure billing problem. At home there were devotions to prepare, a college comparison for my daughter, and a landscaping quote I wanted a second opinion on.

I did all of it with Claude, and the usage problem did not come back. I was never cut off in the middle of something, and I never had to hold work for the next day because the allowance was running low.

I did not notice that right away. What I noticed, somewhere in the middle of the month, was that I had not opened ChatGPT in several days, and the reason I opened it was to run a research prompt Claude had written. The results went back to Claude to be checked against Microsoft's documentation before I used them. That had become the pattern without my deciding on it. Claude was doing the work, and ChatGPT was checking it. In September it had been the other way around. When I mentioned this to my wife, she was not surprised.

Astra was good in that role. On one of those research runs it pointed out that a Microsoft feature one of our procedures relied on had been retired more than a year earlier. It was right, and we would have found out the hard way. I am not leaving ChatGPT because it got worse. I am leaving because a second opinion is not worth a hundred dollars a month to me once the first opinion is right most of the time.

I have two explanations for why the first opinion got better, and I cannot fully prove either one.

The first is that the usage problem stopped. I did not change plans, and I did not change how I work. Either Anthropic adjusted something, or my September work was expensive in a way I could not see, or both. I said in September that my results could not tell me which internal mechanism was consuming my allowance. They still cannot. What I can say is that I did more on Claude in October than in September, with room to spare.

The second is the models. Opus 5.5 is a clear improvement over Opus 5 on the code I hand it, enough that work I was sending to ChatGPT in September now goes to Claude first. Fable 5.1 has handled most of my serious work this month. The console application rework came back in one pass, I ran it, and it worked. After thirty years of shipping software, I know the difference between code I can use and code I have to babysit. In September I was getting too much of the second kind from Claude for what I was paying. This month, mostly, I got the first kind.

The cost picture changed as well. The monthly price is the same on both subscriptions. What changed is that my Max plan now includes API credits, which matters to me because I also build against the API and was paying for that separately. I said in September that the sticker price was not the point, and that what mattered was how much finished work the money bought. That number went up, and it went up on Anthropic's side.

Nothing has changed on privacy and control, and that is fine with me. Those were the reasons I left ChatGPT the first time: what gets retained, what can be used for training, and whether I can see and limit what an agent does with my files and keys. I still want those answers spelled out in the terms of the product I am using, not implied by a company's reputation. On that front Anthropic has always been the easier company to trust. What September showed me is that trust does not make up for a tool I cannot finish my work with. Now that I can, those concerns are back to doing the job I always wanted them to do, which is to break ties in Anthropic's favor.

Reliability was not an issue on either side this month. One month is a small sample, and I mention it only because I said in September that it belonged in the calculation.

I also want to say something about Anthropic itself, because it would be dishonest to leave it out.

I like the company. I like Dario Amodei's story, that he and a group of colleagues left OpenAI because they did not trust where it was going, and started a company on the premise that this technology is powerful enough to be careful with. I like that safety was the founding idea and not something added later. I am hoping they do not prove me wrong. A company with this much capability has a responsibility that most companies never have to think about, and I would like to believe the people running this one mean what they say. Nobody will know for years.

None of that is why I am staying. Liking a company is not a reason to pay it a hundred dollars a month, and I would not have been comfortable writing this post if it were. My wife and I garden, and every summer we end up pulling a plant we like because it is crowding out the ones that produce. Software is no different. The mission is why I wanted Anthropic to win this comparison. The usage fix and the models are why it did. If the usage problem returns, or OpenAI ships something that is better at the work I actually do, I will write that post too, and I already know what my wife will say about it.

I also wrote in September that my experience as a customer had left me feeling that my business mattered less than I thought. I am not taking that back. It was true of that month. This month was different, and I would rather you have both than only the one that makes me look consistent.

The September post stays up as I wrote it, with a note at the top pointing here.

I Built a Free Devotional for Families

We wanted to read the Bible together as a family, but preparing for it was often enough to keep it from happening. On a normal weeknight, I needed to choose a passage, come up with questions, and be ready to talk through the answers. By the time I had thought about all of that, it felt like another task to fit into an already full evening.

I have written software for close to thirty years, so I decided to build something that would help. I wanted to be able to sit down with my family and have the reading and questions ready. That became My Family Devotion, a free site for families to read aloud together.

Each devotion includes:

  • The full Bible passage.
  • A short devotional reading to share aloud.
  • Four discussion questions with suggested answers.
  • A practical application.
  • A closing prayer.

The suggested answers are there to help the person leading the conversation. You can read them when you need them, and there is room for your family to talk through the passage in its own words.

The site currently has 2,335 devotions across all 66 books of the Bible. You can choose a book and work through its passages in order, including the ones that rarely turn up in a collection of favorite verses. I wanted the devotions to help families spend time with those passages too.

Some devotions include a short section called How We Know. It looks at evidence or background relevant to the passage, such as an ancient text, a custom, or an archaeological find. That part reflects my interest in apologetics. I include a note where there is useful evidence to discuss, with sources for claims from outside the Bible so readers can check them.

I use AI to help with drafting and research. I check each Scripture passage against the Berean Standard Bible, verify outside claims, and review and edit the writing before publication. The responsibility for what appears there is mine. If you find a mistake, please let me know.

The devotions focus on reading and discussing Scripture together. They reflect my Christian convictions, but I have kept debates about baptism, church government, and end-times interpretations out of them. Families can discuss those questions within their own church traditions.

There is no account to create, and every devotion is free to read. It will stay that way. If you would like to try it with your family, the getting started guide explains how to choose a reading and work through it together.

C# Export to Excel: Keep Your Dates, Numbers, and Leading Zeros

An Excel export can open without an error and still be wrong. An identifier loses its leading zeros. A date sorts as text. A long reference number comes back with different digits at the end. The download succeeded, but the person using it has a problem.

I wrote about exporting MySafeInfo data to Excel in C# back in 2016. That example used ASP.NET Web Forms and EPPlus. For this one, I wanted a complete, current example with explicit decisions about what goes into each cell.

The source is the States dataset from MySafeInfo, the reference-data service I built. It is available in full without an account or API key, so you can run this example as written. The workbook uses ClosedXML, an MIT-licensed library that creates real .xlsx files without requiring Excel to be installed.

The important choices are small enough to explain before the code:

  • Identifiers are text. The source ID identifies a record; nobody needs to add the IDs together. Names, abbreviations, and capitals are text too.
  • The source's Statehood value is a numeric year. It stays a number. Turning 1819 into January 1, 1819 would invent information the API did not provide.
  • The retrieval timestamp is an Excel date value with a display format. Its label explicitly says UTC, because the spreadsheet cell does not retain a time-zone offset.
  • An empty capital becomes a blank cell. A zero in the source year remains zero. Those are different values, and the exporter should not quietly decide they mean the same thing.

At the time of testing, the States endpoint returned 56 records, including territories and Washington, D.C. The code exports the records it receives; it does not assume this is a list of exactly 50 states or reinterpret the source's year values.

This is a .NET 10 console application using ClosedXML 0.105.1, the version tested here. Create the project and add the package:

dotnet new console --name MySafeInfoExcel --framework net10.0
cd MySafeInfoExcel
dotnet add package ClosedXML --version 0.105.1

Replace Program.cs with the following. It fetches the data, validates the fields, and writes two worksheets: the records themselves and a short record of where and when the data was retrieved.

using System.Globalization;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using ClosedXML.Excel;

try
{
    if (args.Length > 1)
        throw new ArgumentException("Usage: dotnet run -- [output.xlsx]");

    // One client for this one-shot console application.
    using var http = new HttpClient(new HttpClientHandler
    {
        AllowAutoRedirect = false
    })
    {
        Timeout = TimeSpan.FromSeconds(30),
        MaxResponseContentBufferSize = 1_048_576
    };
    http.DefaultRequestHeaders.Accept.ParseAdd("application/json");
    http.DefaultRequestHeaders.UserAgent.ParseAdd("MySafeInfoExcelExample/1.0");

    // Optional: States is available in full without a key.
    string? key = Environment.GetEnvironmentVariable("MYSAFEINFO_API_KEY");
    if (!string.IsNullOrWhiteSpace(key))
        http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", key.Trim());

    string path = Path.GetFullPath(args.Length == 1 ? args[0] : "states.xlsx");
    int count = await StateExport.RunAsync(http, path);
    Console.WriteLine($"Wrote {count} records to {path}");
    return 0;
}
catch (Exception ex) when (ex is HttpRequestException or JsonException
    or IOException or UnauthorizedAccessException or ArgumentException
    or InvalidDataException or FormatException or OperationCanceledException)
{
    Console.Error.WriteLine($"Export failed: {ex.Message}");
    return 1;
}

public static class StateExport
{
    public const string SourceUrl =
        "https://mysafeinfo.com/api/data/states?format=json&full=true";
    public const int MaxRows = 10_000;

    public static async Task<int> RunAsync(HttpClient http, string outputPath,
        CancellationToken cancellationToken = default)
    {
        string path = Path.GetFullPath(outputPath);
        if (!string.Equals(Path.GetExtension(path), ".xlsx",
            StringComparison.OrdinalIgnoreCase))
            throw new ArgumentException("The output file must end in .xlsx.");
        if (File.Exists(path))
            throw new IOException("The output file already exists. Choose a new name.");

        // Buffering keeps the client's response-size limit in effect.
        using var response = await http.GetAsync(SourceUrl,
            HttpCompletionOption.ResponseContentRead, cancellationToken);
        response.EnsureSuccessStatusCode();

        await using var json = await response.Content.ReadAsStreamAsync(cancellationToken);
        var rows = await JsonSerializer.DeserializeAsync<List<StateRow>>(
            json, new JsonSerializerOptions { RespectNullableAnnotations = true },
            cancellationToken) ?? throw new InvalidDataException("The API returned null.");
        Validate(rows);
        DateTimeOffset retrievedUtc = DateTimeOffset.UtcNow;

        // Finish the workbook before giving it its final name.
        string temporary = Path.Combine(Path.GetDirectoryName(path)!,
            $".{Guid.NewGuid():N}.tmp");
        try
        {
            using (var file = new FileStream(temporary, FileMode.CreateNew,
                FileAccess.ReadWrite, FileShare.None))
            {
                WriteWorkbook(rows, file, retrievedUtc);
            }
            cancellationToken.ThrowIfCancellationRequested();
            File.Move(temporary, path); // Does not overwrite an existing file.
        }
        finally
        {
            if (File.Exists(temporary))
                File.Delete(temporary);
        }
        return rows.Count;
    }

    public static void WriteWorkbook(IReadOnlyList<StateRow> rows,
        Stream output, DateTimeOffset retrievedUtc)
    {
        Validate(rows);
        using var workbook = new XLWorkbook();
        var sheet = workbook.Worksheets.Add("States");
        string[] headers = ["Source ID", "State / territory", "Abbreviation",
            "Capital", "Statehood (source year)"];
        for (int column = 0; column < headers.Length; column++)
            sheet.Cell(1, column + 1).Value = headers[column];

        for (int index = 0; index < rows.Count; index++)
        {
            var item = rows[index];
            int row = index + 2;
            sheet.Cell(row, 1).Value = item.Id.ToString(CultureInfo.InvariantCulture);
            // Rich text preserves literal leading apostrophes in source strings.
            sheet.Cell(row, 2).GetRichText().AddText(item.StateName);
            sheet.Cell(row, 3).GetRichText().AddText(item.Abbreviation);
            if (item.Capital.Length > 0)
                sheet.Cell(row, 4).GetRichText().AddText(item.Capital);
            sheet.Cell(row, 5).Value = item.Statehood;
        }

        sheet.Range(2, 1, rows.Count + 1, 1).Style.NumberFormat.Format = "@";
        sheet.Range(2, 5, rows.Count + 1, 5).Style.NumberFormat.Format = "0";
        sheet.Range(1, 1, rows.Count + 1, 5).CreateTable("StatesTable");
        sheet.SheetView.FreezeRows(1);
        sheet.Column(1).Width = 12;
        sheet.Column(2).Width = 30;
        sheet.Column(3).Width = 16;
        sheet.Column(4).Width = 26;
        sheet.Column(5).Width = 26;

        var info = workbook.Worksheets.Add("Export info");
        info.Cell("A1").Value = "Source";
        info.Cell("B1").Value = SourceUrl;
        info.Cell("A2").Value = "Retrieved (UTC)";
        info.Cell("B2").Value = retrievedUtc.UtcDateTime;
        info.Cell("B2").Style.NumberFormat.Format = "yyyy-mm-dd hh:mm:ss";
        info.Cell("A3").Value = "Records";
        info.Cell("B3").Value = rows.Count;
        info.Cell("A4").Value = "Scope";
        info.Cell("B4").Value = "Includes territories and Washington, D.C.";
        info.Cell("A5").Value = "Year values";
        info.Cell("B5").Value = "Copied from the source, including zero; not full dates.";
        info.Column(1).Width = 22;
        info.Column(2).Width = 80;
        info.Range("A1:A5").Style.Font.Bold = true;
        workbook.SaveAs(output);
    }

    private static void Validate(IReadOnlyList<StateRow> rows)
    {
        if (rows.Count is 0 or > MaxRows)
            throw new InvalidDataException($"Expected 1 to {MaxRows} records.");

        var ids = new HashSet<int>();
        foreach (var item in rows)
        {
            if (item is null || item.Id <= 0 || !ids.Add(item.Id)
                || string.IsNullOrWhiteSpace(item.StateName)
                || string.IsNullOrWhiteSpace(item.Abbreviation)
                || item.Capital is null || item.Statehood is < 0 or > 9999
                || item.StateName.Length > 32_767
                || item.Abbreviation.Length > 32_767
                || item.Capital.Length > 32_767)
                throw new InvalidDataException("Unexpected States data; no export was saved.");
        }
    }
}

public sealed record StateRow
{
    [JsonPropertyName("ID")]
    public required int Id { get; init; }
    public required string StateName { get; init; }
    public required string Abbreviation { get; init; }
    public required string Capital { get; init; }
    public required int Statehood { get; init; }
}

Run it with an output filename:

dotnet run -- states.xlsx

The destination directory must already exist. If the file exists, the program refuses to overwrite it; use another filename. A failed request or invalid response produces an error and a nonzero exit code. The final filename is created only after the workbook has been saved successfully.

No key is needed for this dataset. If you have a pass, the optional MYSAFEINFO_API_KEY environment variable sends your key in the authorization header. Keep it out of source code and URLs. The request also uses full=true, which asks MySafeInfo to fail rather than silently return a sample when full access requires a key. Other datasets have different fields and need their own model and column mapping; changing the URL alone is not enough. The API documentation covers access and request limits.

The States IDs do not contain leading zeros. To see why the text decision matters elsewhere, this small, separate workbook uses a postal code and a long reference number:

using ClosedXML.Excel;

using var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add("Text examples");
sheet.Cell("A1").Value = "Postal code";
sheet.Cell("A2").Value = "00501";
sheet.Cell("B1").Value = "Reference";
sheet.Cell("B2").Value = "123456789012345678";
sheet.Range("A2:B2").Style.NumberFormat.Format = "@";
workbook.SaveAs("text-examples.xlsx");

Run that separately, with an unused filename. Both values are assigned as strings. The text format makes the intent explicit, but formatting is not what preserves the original characters. If the application already converted 00501 to the number 501, changing the cell's format cannot recover what was lost. Excel also has a 15-digit limit on numeric precision, so long identifiers belong in text cells.

Dates work the other way around. The export assigns a DateTime to the timestamp cell, then gives it the format yyyy-mm-dd hh:mm:ss. It does not call ToString() and hand Excel a date-shaped piece of text. As the ClosedXML formatting documentation explains, a number format changes presentation, not the underlying value.

Testing caught another detail worth keeping: assigning a string through Value can treat a leading apostrophe as Excel's text prefix. The main export uses GetRichText().AddText() for the API's text fields so a literal apostrophe survives. The tests also check that strings beginning with =, +, -, and @ remain text. Imported text never gets assigned to FormulaA1.

The verification went beyond checking that a file appeared. The final example was run against the live API without a key, the saved workbook was reopened, and its structure was checked with the Open XML SDK validator. Automated cases covered:

  • Text and numeric cell types, leading zeros, an 18-digit reference, Unicode, literal apostrophes, and formula-like text.
  • UTC timestamp conversion and results under U.S., German, and French culture settings.
  • Missing fields, nulls, duplicate IDs, invalid JSON, oversized responses, and too many records.
  • HTTP errors, including 429, cancellation, timeouts, and protecting an existing destination file, including one created during the request.

This is deliberately a bounded export. The HTTP response is capped at one MiB, the request times out after 30 seconds, and validation rejects more than 10,000 rows. Those are example limits, not promises about how much memory a workbook will use. ClosedXML builds the workbook in memory. For a large export or a web application serving concurrent downloads, measure the workload and choose an appropriate background-job or streaming design. Reuse or manage HttpClient through the application's normal lifetime management. The single client here lives for the duration of this console run.

The example also stops on a rate-limit response instead of repeatedly calling the API. A scheduled integration needs a retry policy that respects Retry-After, along with whatever monitoring the job requires.

The next time you check an export, try doing something with it. Sort the dates. Filter a numeric column. Read an identifier back and compare it with the source. Those checks tell you much more than whether Excel managed to open the file.

I Was Asked for Three Books. I Couldn't Do It.

Someone asked me recently for the three books, besides the Bible, that taught me the most about defending my faith. I could not do it. Every time I got the list down to three, I had to cut one that changed how I think, so I stopped narrowing and kept them all.

  • Mere Christianity by C.S. Lewis is where I would start with someone exploring Christianity. Lewis begins with ordinary human experience, including our sense of right and wrong, and works toward the Christian understanding of God and how we should live.

  • More Than a Carpenter by Josh and Sean McDowell is a shorter introduction, focused on Jesus and the evidence for his identity and resurrection. It is small enough to read in an afternoon and hand to a friend.

  • I Don't Have Enough Faith to Be an Atheist by Norman Geisler and Frank Turek makes a broader case, working through questions about truth, God's existence, and the evidence for Christianity. I value the way it develops the argument in steps, rather than treating each objection as an isolated question.

  • The Case for Christ by Lee Strobel approaches the evidence through interviews with scholars. Strobel asks about the reliability of the Gospel accounts and the case for the resurrection, then gives the scholars room to explain their answers.

  • Cold-Case Christianity by J. Warner Wallace examines the Gospel accounts using the methods he brought to his work as a cold-case homicide detective. Questions about witnesses, corroboration, and the handling of evidence give it a different approach from the other books here.

  • Tactics by Greg Koukl is the most practical book on this list for having a conversation about faith. It helps with asking questions, understanding what someone means, and examining a claim without turning the discussion into a fight. Knowing an argument does not necessarily mean knowing how to bring it into a conversation.

  • Love Your God with All Your Mind by J.P. Moreland makes the case that developing the mind is part of Christian discipleship. Reasoning carefully about what I believe belongs in my life as a Christian, including when no one is asking me to defend it.

  • The Story of Reality by Greg Koukl presents Christianity as an account of the world, from its beginning through its final resolution. It helps explain how the beliefs fit together, which can get lost when a conversation moves from one objection to the next.

  • Seven Days That Divide the World by John Lennox addresses Genesis and science, including different interpretations of the creation account. Science comes up more than any other subject when people find out I take Christianity seriously, and I appreciate Lennox's care with both the biblical text and the scientific questions.

  • Can Science Explain Everything? by John Lennox is a shorter book on the relationship between science and Christian belief. It examines what science can explain and whether those explanations leave any reason to believe in God. I would suggest this one for someone who wants to begin with that question rather than the interpretation of Genesis.

If I had to give someone just three to begin with, I would choose Mere Christianity for an introduction to the faith, I Don't Have Enough Faith to Be an Atheist for a fuller examination of the arguments, and Tactics for someone who already believes but goes quiet whenever the subject comes up.

Thirty Years of Code, Five Lessons I Keep Using

I started writing software in the mid-1990s. I've worked through changes in languages, frameworks, and database versions ever since. Looking back, five lessons stand out in the way I work now. Some took much longer to learn than others.

  1. Know what actually runs. When I write a query, I want to see its execution plan and understand which indexes it uses. If it is slow, I want to investigate the statement the database received. That is a large part of why I still use Dapper. I'm comfortable working directly in SQL, and I want to be able to follow a query from the application into the database when I need to diagnose a problem.

  2. The data outlives the code. This took me longer to fully respect. I recently moved an application first written in 2013 onto current .NET, and the production database under it did not change at all. The schema I had designed years earlier was still doing its job after the original application was replaced. Database design deserves care on its own terms, because the application being built today may be only one of several that eventually use it.

  3. Test the way the application actually runs. Before changing a piece of authentication logic recently, I ran a read-only check across thousands of real accounts, connecting the way the real application connects. The connection path was part of what I needed to verify. A simplified test could exercise the logic and still miss a problem caused by how the application accessed those accounts. For that change, I needed evidence from the conditions it would actually encounter.

  4. Understanding the business often takes more work than writing the code. Early on, I expected the difficult part to be the implementation. Over time, I became more concerned with understanding what the business needed and keeping those rules clear in the application. When a rule is scattered across database logic and screen behavior, even a small change can require looking in several places to understand what it will affect. I want to be able to find the rule, explain it, and change it without having to rediscover how the business works.

  5. Keep reviewing my own decisions. During a redesign of My New Password, I found a security bug in the password generator I had written years earlier. I used the site frequently myself, and the passwords looked as random as I expected. It took looking at the implementation again to see the problem. I want to give code I wrote years ago the same attention I would give a change I was reviewing today.

One Verse I Keep Coming Back To

Paul says he learned to be content. That word, learned, is the one I keep coming back to. I find it encouraging that he describes something he had to learn, because contentment can seem like a quality some people simply have and others don't.

He wrote the letter while imprisoned. In the first chapter, he considers both the possibility of death and the work he could continue if he lived. He wants to be with Christ, but he also wants to remain and help the people he is writing to.

And notice it runs both directions. Paul says he knows how to live with little, and he knows how to live with plenty (Philippians 4:12). I tend to think of contentment as something needed when life is hard. Having enough sounds as though it ought to settle the question. But it is possible to have plenty and still be dissatisfied, or to become so comfortable that dependence on God gets little thought.

That is the setting for a verse that often gets separated from the sentences around it:

I can do all things through him who strengthens me. (Philippians 4:13, ESV)

It is easy to read that as encouragement to accomplish whatever we set our minds to. Read it after Paul's description of hunger and plenty, though, and the claim is more specific. He depends on Christ for the strength to live through either. His confidence does not depend on getting the outcome he wants. That makes the verse useful even when a situation fails to improve.

Paul also thanks the Philippians for helping him. Immediately after verse 13, he acknowledges their kindness in sharing his trouble, and a few verses later he says their gifts have supplied his needs. He can be content and still appreciate relief. There is no need to pretend hunger is harmless or that another person's help makes no difference.

I keep coming back to this passage because most days I am nearer the plenty than the prison, and that is exactly when I forget. I can read it as encouragement for someone going through a hard time and miss how much it has to do with me. Paul includes plenty among the circumstances in which he relies on Christ. I need that dependence when life is comfortable too.