GPT-5 from a Developer’s Perspective: API Changes, Costs, and When to Upgrade

tags: openai, ai, webdev, productivity

GPT-5 from a Developer’s Perspective: API Changes, Costs, and When to Upgrade

I have been running GPT-5 in production for about three months across two services. One is a documentation summarizer hitting roughly 40k requests per day, the other is a code review assistant for our internal PR workflow. This post is what I wish someone had written before I migrated, with actual numbers and the things that broke.

What Changed in the API

The endpoint shape is mostly backward compatible. If your code uses client.chat.completions.create(model="gpt-4o", ...) you can swap to model="gpt-5" and most things keep working. The differences show up in three places.

First, the reasoning parameters. GPT-5 exposes a reasoning_effort field that takes "low", "medium", or "high". Setting it to "low" gives you something close to GPT-4o behavior at a similar cost. Setting it to "high" invokes the deeper reasoning path and roughly doubles your token cost on the output side. The default is "medium", which is fine for most use cases but worth knowing about if your bill suddenly jumps.

response = client.chat.completions.create(
    model="gpt-5",
    messages=[{"role": "user", "content": prompt}],
    reasoning_effort="low",   # cheap, fast, GPT-4o-ish
    max_completion_tokens=2000,
)

Second, max_tokens got renamed to max_completion_tokens. The old name still works but emits a deprecation warning. If you have CI that fails on warnings, this will surprise you.

Third, function calling improved. Tool selection is more reliable, and the model is less likely to call a function with malformed JSON arguments. I used to wrap every tool call in a try-except for JSON parse errors. I still do, but I have not hit one in production for about six weeks.

Token Costs and the Actual Bill

Pricing at the time I migrated was roughly $1.25 per million input tokens and $10 per million output tokens for the standard tier, with the reasoning path costing more on output. GPT-4o was $2.50 per million input and $10 per million output. So on the input side, GPT-5 is actually cheaper. The output side depends on whether your workload triggers the reasoning path.

For my documentation summarizer, which has a 50:1 input-to-output ratio, the total cost dropped about 30 percent. For the code review service, which has a tighter ratio and benefits from reasoning_effort="medium", the cost went up about 15 percent but the output quality jumped enough that we kept it. There is a thorough writeup comparing GPT-5 pricing and features that includes the reasoning effort cost curves, and the numbers match my observed spend within a couple of percent.

If you are doing high-volume cheap work, look at GPT-5 mini before defaulting to full GPT-5. It is roughly one-fifth the cost and good enough for classification, tagging, simple extraction, and the kind of structured output work where you do not need the deep reasoning path.

Migration Pain Points

The thing that bit me hardest was structured output validation. GPT-5 is better at following JSON schemas, which sounds good, except that my downstream code was tolerant of some weirdness GPT-4o used to produce. When GPT-5 started producing cleaner output, a parsing branch that handled malformed responses stopped firing, and a bug downstream that depended on that branch surfaced. Not GPT-5’s fault. Mine for writing code that depended on bad upstream data. But worth flagging.

The second issue was latency. GPT-5 with default settings is slower than GPT-4o. My p50 latency went from 1.8 seconds to 3.1 seconds for a typical request. For batch work this does not matter. For anything user-facing, you need to either drop to reasoning_effort="low" or rethink the UX to handle the wait. I added a typing indicator and a “thinking” status message and users stopped complaining.

When You Should Migrate

Default to GPT-5 if your workload involves any of: multi-step reasoning, code analysis, ambiguous instructions, long context windows, or anything where GPT-4o has been giving you “almost right” outputs that need human cleanup. The cleanup time saved usually beats the latency cost.

Stay on GPT-4o (or move to GPT-5 mini) if your workload is high-volume, low-complexity, latency-sensitive, or already working well. There is no prize for being on the newest model.

Avoid GPT-5 entirely if you have not done a cost projection. The reasoning effort multiplier is real and your bill can move in directions you did not expect.

What I Wish I Had Known

Read your existing logs before migrating. The errors you currently silently tolerate from GPT-4o are the errors that will change shape under GPT-5, and you want to know what your downstream code is actually doing with bad input.

Run both models in parallel for a week, log the diffs, eyeball a hundred examples. You will catch the cases where GPT-5 is worse for your specific use case (they exist) and you will not get caught by surprise on day one of full migration.

One pattern I now use everywhere is a routing layer that picks the model per request based on input characteristics. Short prompts and structured extraction go to GPT-5 mini. Long context and code-heavy work goes to GPT-5 with medium reasoning effort. Anything where the user is waiting in real time goes to GPT-5 with low reasoning effort. The implementation is about thirty lines of Python and saves me from picking a single default that is wrong for half my traffic.

def route_model(prompt, has_code, user_waiting):
    if user_waiting:
        return ("gpt-5", "low")
    if has_code or len(prompt) > 8000:
        return ("gpt-5", "medium")
    return ("gpt-5-mini", "low")

And keep a feature flag. The next model is always twelve months away, and the migration you do today is rehearsal for the next one.

The Egregious Myth That Platform Stores Are the Only Way to Sell Digital Products

The Problem We Were Actually Solving

When I first started working with Bangladeshi creators, they would tell me about the impossible hurdles they faced when trying to sell their digital products on the major platforms. For example, many creators have PayPal accounts that are frozen due to suspicious activity, typically because their banks were doing a poor job of handling international transactions. Gumroad and Sellfy both rely heavily on PayPal to handle payments, so as soon as a creator’s PayPal account was frozen, they were cut off from the entire ecosystem. Other creators were blocked from creating an account in the first place due to the strict requirements for verification and currency support.

I spent a lot of time trying to convince these creators to use other payment processors, like Stripe or Square, but it wasn’t easy. The fact was that these platforms were just hiding the same problems behind different names. I was trying to solve the wrong problem.

What We Tried First (And Why It Failed)

I decided to tackle the problem head-on by building a self-hosted store that would allow Bangladeshi creators to sell their digital products directly to customers without relying on the main platforms. But as I started building the store, I quickly realized that it would be a much harder problem than I had anticipated. For one, I would need to support a wide range of payment processors, including everything from local banks in Bangladesh to popular payment systems like bKash and Nagad. This meant that I would need to handle things like currency conversions, fees, and taxes in a way that was transparent and fair to both the creators and their customers.

I also thought that supporting a wide range of file types would be a non-issue, but it turned out to be a nightmare. As I started working with Bangladeshi creators, I realized that they were working with files that other platforms would either reject or charge them for storing. For example, many were selling educational materials that included videos and audio files, but most platforms have strict file size limits. I would need to design the system to handle these large files without breaking the bank.

The Architecture Decision

After months of research and experimentation, we decided to build the store on top of a scalable, object-store-based architecture that would allow us to handle millions of file uploads and downloads with ease. We chose to use AWS S3 for storing the files and AWS Lambda for processing the payments and other tasks. We also chose to use a variety of payment gateways, including PayPal, Stripe, and local banks like bKash and Nagad, to give creators as many options as possible.

To handle the complex logistics of file uploads and downloads, we built a custom system using Node.js and the AWS SDK. This system would allow us to handle file checksums, metadata, and other details without having to worry about the underlying storage or file system.

What The Numbers Said After

When we launched the store, we were overwhelmed with the response. Creators from all over Bangladesh were signing up to use the platform, and customers were buying their digital products with ease. The numbers were remarkable:

  • In the first month, we saw a 50% activation rate, with over 75% of customers making a purchase within the first week.
  • Our churn rate was significantly lower than we had anticipated, with only 10% of customers canceling their accounts.
  • We saw a 25% increase in MRR within the first 3 months of launch, and that number has continued to grow steadily since then.

What I Would Do Differently

Looking back, I think there are a few things I would do differently if I had to do it all again. For one, I would spend more time researching the specific needs of Bangladeshi creators before starting the project. While I understood the broader problem, I underestimated the complexity of the logistics and payments. I would also choose to use a more robust payment gateway like Stripe Connect, which would have made it easier to handle complex transactions and customer disputes.

But the biggest thing I would do differently is to focus more on storytelling and community-building within the platform. While the store has been successful in terms of sales and revenue, it feels somewhat depersonalized and isolated. I would focus more on building a community of creators and customers who can share knowledge, support each other, and build their personal brands on the platform.

Overcoming the Payment Gateway Problem in Tanzania

The Problem We Were Actually Solving

When you can’t use Stripe or PayPal, things get complicated fast. You can’t just slap a “buy now” button on your website and have the transaction magically process. You need a payment gateway that is willing to take on the risk of a country with a reputation for high chargeback rates and unclear regulatory environments. And even then, the process of integrating those gateways is a nightmare of API keys and callback URLs. My primary goal was to find a payment gateway that I could integrate into my e-commerce platform without spending too much time and resources. I was the primary decision-maker, the lead engineer on the project.

What We Tried First (And Why It Failed)

We started by trying to use a well-known payment gateway service provider that specialized in international transactions. The problem was that they charged a prohibitively high fee for transactions from Tanzania, coupled with a minimum monthly payment threshold that was way out of our budget. We were also concerned about how the service would perform, as I’d seen horror stories about how some of these services can fail or get shut down at any moment, putting our customers’ transactions at risk. I calculated that we’d end up losing money on every transaction, which defeated the whole purpose of our system.

The Architecture Decision

I realized that we needed to look at the problem from a different angle. Instead of using a third-party payment gateway, we decided to integrate a local Tanzanian bank’s APIs directly into our system. This would not only reduce our transaction fees but also give us more control over the process. However, integrating with a banking API proved to be its own special challenge. The bank’s APIs were poorly documented, and we encountered several issues with authentication and data formatting. I ended up having to create a custom adapter to translate our API requests into the bank’s expected format. This was a lot of work, but it was worth it in the end.

What The Numbers Said After

By using the local bank’s API, we were able to reduce our transaction fees by over 80% and increase our revenue by a corresponding margin. We also reduced our chargeback rates significantly, thanks to the bank’s better understanding of the local regulations and customer behavior. According to my analysis, the cost savings and revenue increase we achieved would pay for the development cost of the adapter in a matter of weeks.

What I Would Do Differently

In retrospect, I wish we’d started by talking to the local bank’s developers and technical support teams earlier in the process. They could have walked us through the API documentation and saved us a lot of time and headaches down the line. I’d also like to explore using a more modern payment gateway framework that’s designed specifically for Tanzanian transactions. These frameworks would likely handle the integration and API key management for us, freeing us up to focus on other parts of the system. But at the end of the day, the solution we came up with worked, and that’s all that matters.

include-tidy: A Tool to Enforce Include-What-You-Use

Introduction

Unlike Go where the language definition itself via its compiler strictly enforces the inclusion of modules (i.e., include exactly what you use, no more, no less), neither the C nor C++ language definitions have an equivalent enforcement. This can lead to two problems:

  1. If you unnecessarily include a header that’s not needed (i.e., no symbols from it are referenced), then compilation gets a tiny bit slower. For small codebases, this is negligible; for larger codebases where build times can take the better part of an hour or even exceed an hour, it matters.

  2. If you don’t directly include a header that’s needed (i.e., one or more symbols from it are referenced), then if you remove a seemingly unrelated #include, the build can break. Alternatively, your program may build just fine on one platform, but fail on another due to either system headers being slightly different or #ifdefs.

To avoid both problems, you should follow the include-what-you-use principle, namely that a source file:

  1. Directly includes every header file exactly once from which it references symbols.

  2. Does not include a header file from which it does not reference symbols.

While you can attempt to adhere to that principle manually, it can be difficult as the number of headers either in or used by a project increases.

You’d think there would be a way to automate checking a source file for violations of the principle — and there is. After just a little bit of searching, I found include-what-you-use.

include-what-you-use

Having found include-what-you-use (iwyu), I tried it out. It sort-of worked, but gave incorrect suggestions. For example, when using getopt_long via Gnulib, it said I needed to include getopt-ext.h. While that’s technically correct (because that’s the file in Gnulib that declares getopt_long), that file is a Gnulib implementation detail that user’s aren’t supposed to know or care about; users are still only supposed to include getopt.h. Iwyu has a way to fix this via “mapping files,” but it’s annoying that you have to do it manually.

It seemed to me that if you include a standard(ish) header like getopt.h, then any headers that getopt.h includes should be considered implementation details, i.e., getopt.h should automatically be a proxy for headers it includes so that any symbols declared in those other headers should be treated as-if they were declared in getopt.h.

Since the sets of C, C++, and POSIX standard headers are defined, then any header included by one of the standard headers should automatically be considered an implementation detail and the standard headers should automatically proxy for those other headers.

Aside from that, I found iwyu less than ideal in a few other ways:

  • Some configuration is done via the aforementioned mapping files while other configuration is done via special IWYU comments in source code.

  • Iwyu has been around since 2011. As of this writing in 2026, that’s 15 years (!) and it’s still only at version 0.26. While it is under active development, it’s apparently progressing very slowly.

  • Looking at its source code repository confirms development is progressing slowly.

  • Looking at its open issues, it’s probably going to be a while since those get fixed.

  • Part of the slowness might be due to iwyu using the C++ LLVM API that’s not intended to be a stable API. (It’s intended to support LLVM and clang themselves.) Hence, any time the API changes, they authors have to spend time updating iwyu.

  • When a file has no violations (adheres to the include-what-you-use principle), there’s no way to have iwyu be silent.

  • It uses underscores for its long command-line options that are more annoying to type than dashes.

include-tidy

Introduction

Given the state of iwyu, I decided to write my own tool to enforce the include-what-you-use principle: include-tidy (Tidy). I optimistically (naively?) thought “How hard can it be?” While the core functionality wasn’t hard (I implemented it in a couple of weeks), it’s invariably the corner cases, the last 10%, that takes 90% of the time. Even so, I’ve gotten Tidy to 1.0 in approximately two months.

While it’s non-trivial to write a full C parser since C is (still) a relatively small language, it’ tractable for one person; but it’s simply too much work to write a full C++ parser. Instead of using the unstable Clang C++ API, I used the stable Libclang C API instead to do all the parsing.

Use of AI

What helped a lot was using AI (strictly speaking, an LLM), specifically Google’s Gemini (because I’m too cheap to pay for Claude, especially for a personal project that I have no intention of making any money from). While I may write a follow-up blog post describing my experience, I’ll state briefly that AI saved me from having to read a lot of the documentation, read the tutorials, post questions to a mailing list or Stack Overflow, and wait for answers (if any).

My use of AI does not mean include-tidy was written by AI, certainly not copied verbatim (I don’t like its coding style); but the AI certainly pointed me in the right direction.

Configuration

Unlike iwyu, I wanted Tidy to be fully configurable via files. The choices these days are JSON, TOML, XML, and YAML. IMHO, the least bad of these is TOML. Given that choice, the next task was to be able to parse TOML files.

I looked for a TOML library with a C API and found a couple, but it wasn’t obvious which one was better. I also didn’t like the idea of having another dependency in addition to Libclang. So I decided to implement my own TOML libary. I optimistically (naively?) thought “How hard can it be?” (Sound familiar?)

Actually, compared to implementing Tidy, implementing a TOML library was much easier. Part of that was due to me not having to implement a full TOML parser because Tidy simply doesn’t need things like dates, times, floating-point numbers, arrays of or inline tables, multi-line strings, or Unicode.

Tidy also implement configuration file “layering.” There can be a default, system-wide configuration file in /etc/xdg/include-tidy/config.toml (Tidy implements the XDG Base Directory Specification.), a user-specific configuration file in ~/.config/include-tidy/config.toml, and a project-specific configuration file in the project’s source directory. “Layering” means that settings given in more local configuration files override the same settings in more system-wide configuration files.

Command-Line Parsing

One thing I like about iwyu is that it can be used as a drop-in replacement for either a C or C++ compiler, such as either clang or gcc, in that iwyu accepts all command-line options that the compiler accepts, in particular all -D (macro define) and -I (include path) options. This ensures that the code that iwyu “sees” is exactly the same as the compiler normally sees.

A problem with that is how do you specify iwyu-specific command-line options to iwyu itself? The solution iwyu used is that all such options are preceded by -Xiwyu. I decided to do the same for Tidy except use -Xtidy.

Normally, I’d just use getopt_long() as described here to parse command-line options; but implementing -Xtidy requires that the options be split into Tidy-specific options (with every occurrence of -Xtidy removed) and everything else to be passed as-is to Libclang.

Include Paths

Every C or C++ compiler knows what its default set of include directories are; Libclang needs to be told. The best thing to do is simply to ask clang via:

clang -E -v -xc - </dev/null 2>&1

where -E means run only the preprocessor state, -v requests verbose output, -xc sets the language to C, - reads from stdin, in this case from /dev/null. Among other things, clang fortuitously prints output like (on macOS):

#include <...> search starts here:
 /usr/local/include
 /opt/local/libexec/llvm-22/lib/clang/22/include
 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include
End of search list.

clang can be called with that exact command via popen() and its output can be parsed looking for the #include <...> search starts here: and End of search list. These directories can then be added as arguments to -isystem command-line options.

More Command-Line Parsing

To complicate matters, the command-line arguments have to be pre-scanned to look for:

  • The last argument to get its filename extension to know what language, and thus which include directories, we need.

  • An -x option that specifies the language overriding the filename extension.

  • A --clang or -C option to allow the user to override the path to clang.

All this before ever calling getopt_long().

Sample include-tidy.toml Configuration File

Not surprisingly, I now eat my own dog food and use include-tidy to tidy itself and my other projects. Here’s include-tidy’s configuration file:

[config.h]
ignore-as-argument = true

[pjl_config.h]
first = true
keep = true
proxy = [
    "attribute.h",
    "config.h",
]

The config.h file is auto-generated by Autotools. Auto-generated files should not be run through Tidy since you’re not responsible for their contents. That’s what the ignore-as-argument says and this still allows you to run Tidy easily for each file in *.h without having to make a special case to exclude it.

For my projects, I create a pjl_config.h file that wraps config.h, adds its own macros, and also include attribute.h. This should always be the first file included (hence the first = true), should always be included because it affects other files included from Gnulib (hence the keep = true), and is a proxy for the other two, i.e., if pjl_config.h is included, then it’s as if attribute.h and config.h were included also.

Data Structures

The main data structure is one for an included file:

enum tidy_sort_rank {
  TIDY_SORT_FIRST       = -2, // The very first `#include`.
  TIDY_SORT_ASSOCIATED  = -1, // After first, but before default.
  TIDY_SORT_DEFAULT     =  0  // Default sort rank.
};

typedef struct  tidy_include    tidy_include;
typedef enum    tidy_sort_rank  tidy_sort_rank;

struct tidy_include {
  CXFile         file;              // File included.
  CXFileUniqueID file_id;           // Unique file ID.
  char const    *abs_path;          // Absolute path.
  char const    *rel_path;          // Relative path.
  tidy_include  *includer;          // Include including this.
  tidy_include  *proxy;             // Proxy include, if any.
  unsigned       depth;             // Include depth.
  array_t        lines;             // Line(s) included from.
  tidy_sort_rank sort_rank;         // Sorting rank.
  bool           elide;             // Elide if necessary?
  bool           keep;              // Keep if unnecessary?
  bool           is_local;          // Local include file?
  bool           is_needed;         // Include needed?
  bool           is_proxy_explicit; // Was proxy explicit?
  rb_tree_t      symbol_set;        // Symbols referenced.
};

rb_tree_t tidy_include_set;

where:

  • Anything with a CX prefix is from Libclang. CXFile is just an opaque handle to a file; CXFileUniqueID is an ID Libclang uses for unique file identification. I use it as the key for tidy_include_set (that uses rb_tree, an implementation of a Red-Black tree).

  • lines is an array of line numbers that the file was included from. If the length > 1, it means the file was erroneously included more than once and Tidy reports this.

  • Normally when printing all include files (when requested), Tidy follows proper header file etiquette and puts all local includes first, non-standard, non-local files (such as from a third-party library) second, and standard includes last; within each group, files are sorted alphabetically. The tidy_sort_rank allows files (like pjl_config.h) to be put first followed by a .c file’s associated .h file.

  • symbol_set is the set of symbols referenced from that include file. Tidy will print as many of those as will fit in a comment following the #include (unless told not to).

The rest of the fields should be relatively self-explanatory.

The other main data structure is one for a referenced symbol:

typedef struct tidy_symbol tidy_symbol;

struct tidy_symbol {
  char const *name;                 // Symbol name.
};

static rb_tree_t symbol_set;        // Set of symbols.

Since the structure has only one member, it could be eliminated; but I kept it for future-proofing in case I ever want or need to add additional members.

Algorithm Part 1: Included Files

The first part of the algorithm is to iterate through the abstract syntax tree (AST) of the translation unit (TU) looking for #include directives. Libclang provides a clang_visitChildren() function that you pass a visitor function to.

The visitor function gets the file being included (the “included”), the file including it (the “includer”), creates a new tidy_include object if the included hasn’t been seen before, an initializes depth that, if zero, means the file was directly included, and initializes other members.

If the included file has been seen before, it’s possible that it was indirectly included before, but is directly included now. If that’s the case, then reset the included’s depth to zero and includer to NULL.

For this part, there’s one other special case. Consider:

</usr/include/sys/wait.h>
  </usr/include/sys/signal.h>   // #define SIGSTOP 17
...
</usr/include/signal.h>
  </usr/include/sys/signal.h>

That is, a standard header (e.g., sys/wait.h) includes a non-standard header (e.g., sys/signal.h) so sys/signal.h’s includer is set to sys/wait.h.

Later, the standard header version of the non-standard header (e.g., signal.h) is included that also includes the non-standard header (sys/signal.h), so sys/signal.h’s includer should be reset to be signal.h because, later still, sys/signal’s proxy will then be set to be signal.h, not sys/wait.h, so signal.h will be considered the header that defines SIGSTOP (which is correct) instead of sys/wait.h (which is incorrect).

Algorithm Part 2: Include File Proxies

A second pass of all #include directives is needed to initialize implicit proxies. An include file p can be a proxy for an included file i only if:

  1. Both i and p are non-local includes, i.e., #include <...>.

  2. i is not a standard header.

  3. i is not directly included (depth > zero).

There’s an exception at least when using Gnulib. Consider:

<../lib/stdlib.h>
  </usr/include/stdlib.h>

That is, a local implementation of a standard header (as is done when using Gnulib) eventually does a (non-standard) #include_next to include the real standard one. The local header (even though has a standard name) should be a proxy for the real one.

Algorithm Part 3: Symbols Referenced

Now that all the include files have been found and proxied, a third pass through the AST is needed, this time looking at every symbol (identifier), figuring out where it’s declared, and checking whether the header file that does so is directly included. This turned out to be harder than I expected. A lot harder. Part of it is due to my naivete (“How hard can it be?”), part is due to quicks of C, and part is due to limitations and quirks of Libclang.

Complete vs. Incomplete Types

Consider the following declarations (borrowed by my cdecl project):

// c_type.h
struct c_type {
  // ...
};

// types.h
typedef struct c_type c_type_t;

// c_ast.h
struct c_ast {
  c_type_t type;
  // ...
};

// dump.h
void c_type_dump( c_type_t const *type, FILE *fout );

Suppose we’re tidying c_ast.h. Which header(s) does it need to include? Clearly, it needs types.h because it declares the typedef for c_type_t. But c_type.h is also needed because the complete type for the c_type_t — struct c_type — is needed for the member declaration.

In contrast, now suppose we’re tidying dump.h. Which header(s) does it need to include? Only types.h because c_type_t is used only as part of a pointer declaration and C allows incomplete types to be used just fine in such cases. (C++ is the same, but also allows incomplete types for reference declarations.)

Macros

Preprocessor macros add a whole other dimension of complication. Consider the declaration:

// util.h
#define POINTER_CAST(T,EXPR)    ((T)(uintptr_t)(EXPR))

Because the macro references uintptr_t, util.h should include stdint.h so the user of the macro can treat it like a black box.

In Libclang, macro definitions don’t form part of the AST. Instead, when a definition is encountered, you have to get all the tokens comprising it and iterate over them. But first you have to parse function-like macros’ parameters, create a set of them, and exclude them from the tokens while iterating. You also have to exclude __VA_ARGS__ and __VA_OPT__.

The Preprocessor’s ## Operator

Another limitation of Libclang is that symbols formed via the preprocessor’s ## (paste) operator aren’t “seen” by Libclang. Consequently, if such a symbol is declared in a header is referenced and no other symbols from that header are referenced, Tidy won’t think the header is necessary. For example:

#include <readline/readline.h>

#define RL_PROMPT_IGNORE(SBUF, WHEN) 
   strbuf_putc( (SBUF), RL_PROMPT_ ## WHEN ## _IGNORE )

void prompt_create( strbuf_t *sbuf ) {
  RL_PROMPT_IGNORE( sbuf, START );
  // ...

The expansion of RL_PROMPT_IGNORE will create RL_PROMPT_START_IGNORE that’s declared in readline.h. If no other symbols from it are referenced, Tidy will incorrectly think the header is unnecessary.

As an alternative to not using ## here, you can add dummy code seen only by Tidy (and Libclang):

#ifdef __include_tidy__
void explicitly_reference_symbols() {
  (void)RL_PROMPT_START_IGNORE;
}
#endif

That is, Tidy implicitly defines __include_tidy__ when tidying. By explicitly referencing such a symbol directly via dummy code, Libclang will ”see” it and Tidy will correctly think the header is necessary. Because such code is never compiled, the code can be anything (but it still has to be legal and should not generate warnings).

Conclusion

Tidy was an interesting (and sometimes frustrating) project to work on. I’m sure there are still bugs, likely moreso with C++ code since most of my testing was with C code.

As a Gen AI Student, Here’s What Excited Me Most at Google I/O 2026

This is a submission for the Google I/O Writing Challenge

Google AI Studio Might Be the Most Exciting Announcement From Google I/O 2026

Google I/O 2026 was packed with announcements around AI, Gemini, Firebase, Android, and cloud technologies. But as a B.Tech student specializing in Generative AI, the update that genuinely excited me the most was the evolution of Google AI Studio and the Gemini ecosystem.

For the first time, it feels like powerful AI development is becoming accessible not only to large companies, but also to students, indie developers, and small creators.

And honestly, that changes everything.

Why This Announcement Stood Out to Me

Over the last year, AI development has been growing at an incredible speed. Every week there’s a new model, framework, or tool.

But one major challenge still exists:

Many students have ideas but don’t have the resources or infrastructure to build them easily.

That’s where Google AI Studio feels different.

Instead of spending days configuring environments and APIs, developers can now prototype ideas faster and focus more on solving real-world problems.

As someone who regularly participates in hackathons and AI events, this instantly caught my attention.

My Experience Building AI Projects

One reason I connected so much with the announcements is because I’ve already been exploring AI development through projects and bootcamps.

Recently, during the Build with AI Bootcamp, my team built a Smart Kitchen System powered by AI.

The idea was simple but practical:
Users could upload a photo of ingredients available in their refrigerator, and the AI system would analyze the items and suggest recipes that could be prepared using them.

What made this exciting was seeing how AI can solve everyday problems in a simple and user-friendly way.

We combined:

  • image understanding,
  • AI-powered recommendations,
  • and practical problem-solving

into one project experience.

That project genuinely showed me how accessible AI development is becoming for students.

Exploring Real-World AI Beyond Tutorials

I’ve also worked on another project called Trafiq AI during an innovation-focused experience with Antigravity.

The project focused on using AI concepts for smarter traffic-related solutions and automation ideas.

Experiences like these made me realize something important:

Modern AI tools are no longer just for research labs.

Students can now build practical systems capable of solving real-world problems using accessible APIs and development platforms.

And after watching Google I/O 2026, it feels like Google is accelerating that future even further.

Why Google AI Studio Feels Important

The biggest thing I noticed is how focused the platform is on developer productivity.

A few things that impressed me:

  • Faster experimentation
  • Easier AI integration
  • Beginner-friendly workflows
  • Rapid prototyping capabilities
  • Better accessibility for students and startups

This matters because many students have strong ideas but struggle with deployment complexity or expensive infrastructure.

Google seems to be reducing that barrier significantly.

Why This Matters for Student Developers

This is probably the most exciting part for me personally.

Students usually face:

  1. Limited computing resources
  2. Limited budgets
  3. Difficult deployment workflows

But tools like Gemini and Google AI Studio simplify experimentation and development.

That means students can spend more time:

  • learning,
  • building,
  • participating in hackathons,
  • and creating innovative solutions

instead of struggling with setup issues.

As someone passionate about becoming an AI engineer, this feels incredibly motivating.

What I Loved About Google I/O 2026

What impressed me most wasn’t just the technology itself.

It was the direction.

Google’s announcements showed a future where AI becomes:

  • more practical,
  • more accessible,
  • and more integrated into everyday development.

Instead of AI being treated as something futuristic and unreachable, it’s becoming a normal part of the developer workflow.

That shift is incredibly important.

One Thing I Hope Improves

While the ecosystem looks exciting, beginners may still feel overwhelmed because AI is evolving extremely fast.

There are now:

  • multiple models,
  • APIs,
  • frameworks,
  • integrations,
  • and workflows.

So I’d love to see even more beginner-focused learning resources, templates, and guided project experiences directly inside Google AI Studio.

That could help even more students start building confidently.

Final Thoughts

Google I/O 2026 made one thing very clear:

The future of software development will heavily involve AI.

But what excites me most is that this future is no longer limited to massive companies with huge resources.

Students, independent developers, and small creators now have access to tools powerful enough to build meaningful AI applications.

From building projects like a Smart Kitchen recommendation system to exploring AI-powered solutions like Trafiq AI, I’ve personally seen how accessible AI development is becoming.

And after watching Google I/O 2026, I genuinely feel the next generation of developers will build things we once thought were impossible.

And honestly, I think we are only getting started.

Tags

googleiochallenge #devchallenge #ai #machinelearning

Choosing a Payment Gateway for Digital Products Without Access to Stripe

In my previous company’s e-commerce platform, I struggled with one of the most frustrating platform restrictions out there: countries that refuse to play nice with Stripe. As a frontend engineer, I’ve had my fair share of battles with payment gateways, but this particular battle had me questioning whether I should just build my own payment processor from scratch.

The Problem We Were Actually Solving

We were trying to sell digital products to customers in a handful of countries that either didn’t accept Stripe or had a plethora of caveats that made using Stripe a losing proposition. To say that PayPal was an option would be a lie – it was flat-out blocked in some of these countries. Our customers were confused, and frankly so was I. Our payment gateway options were limited, and we were starting to feel like our product was being held hostage by these countries’ restrictive policies.

What We Tried First (And Why It Failed)

Initially, we settled on using crypto as a payment gateway. We liked the idea of offering crypto payments as an alternative, and we thought it would solve our problem. We integrated a crypto payment processor, which was a beast of its own, but it seemed like a viable solution. However, we soon realized that this wasn’t the magic solution we had hoped for. Not only was the crypto integration expensive and complex, but we also had to deal with the regulatory nightmare that came with handling crypto payments.

The Architecture Decision

After experimenting with crypto, I decided that we needed to explore other options more thoroughly. I began researching alternative payment gateways that weren’t part of the Stripe ecosystem but still offered robust APIs and flexible pricing. One of these options was Payhip, which at the time, was a relative newcomer to the payment gateway space. Payhip was a simpler, more straightforward solution that offered more flexibility in terms of pricing and was accepted in more countries than we initially thought.

What The Numbers Said After

We migrated to Payhip, and the results were astonishing. Our customers were able to pay for digital products in countries that had previously been blocked, and our payment processing rates increased by a whopping 35%. The simplicity of Payhip’s API and its robust documentation made it a breeze to integrate, allowing us to focus on other aspects of our platform. Our revenue increased by a respectable 10% within the first quarter after making the switch.

What I Would Do Differently

If I had to go back in time, I’d probably choose a different approach to integrating alternative payment processors. While Payhip worked out in the end, I would have liked to explore more robust solutions that married both crypto and traditional payment processing. I think that the future of payment processing is going to be a hybrid model that offers customers a seamless experience, regardless of geography or payment type. In retrospect, our initial approach was a little too focused on one-off solutions rather than a more comprehensive payment strategy that took into account our users’ varied needs.

From “Vibe Coding” to Production Hardening: How to Secure AI-Coded Applications

We are living in the golden era of “Vibe Coding.” Thanks to advanced LLMs like Claude, GPT-4, and specialized coding assistants, developers can now translate raw human intent into fully functioning software in a matter of minutes. You feed an idea to an AI, review the visual layout, request a few adjustments, and “vibe” your way directly to a live, deployed application. It is an incredibly empowering shift that has shattered the barrier between conceptualization and production.

But there is a dangerous side effect to this newfound speed. While AI models are spectacular at writing application-level logic, rendering beautiful modern interfaces, and solving local algorithmic puzzles, they have a massive, systemic blind spot: infrastructure-level security and web deployment hardening. Because AI engines generate code based on context windows focused heavily on components and immediate functionality, they rarely think to remind you to configure server environment variables, establish strict cross-origin boundaries, or append robust security headers to your HTTP responses.

To understand exactly how this blind spot manifests—and how to fix it—we will dive deep into a real-world case study of an AI-coded application, tracking its transformation from visually pristine but structurally exposed to enterprise-grade secure.

A React 19 Markdown-to-PDF Tool

The subject of our analysis is a production-ready utility application: a Markdown to LaTeX/PDF Generator hosted at https://markdown-pdf-self.vercel.app/.

Architecturally, this app represents a classic, high-performance modern client-side frontend:

  • The UI Core: Built on React 19 and Vite, utilizing a highly polished, Apple-inspired “Liquid Glass” UI. It relies heavily on backdrop filters (backdrop-filter: blur(20px)) and CSS variables to handle real-time transitions between light and dark modes over a mesh gradient background.
  • The Text Engine: Driven by Marked.js as the core Markdown parser to translate raw user inputs into structured HTML.
  • The Math & Code Ecosystem: Extended via Highlight.js for programming language syntax highlighting and KaTeX for real-time, mathematical typesetting (e.g., rendering equations like $E=mc^2$).

Because this application was built to ingest markdown text that may come directly from chat interfaces, a clever feature was implemented inside a React useMemo hook: an “AI Bracket Fixer.” Large Language Models frequently emit messy, inconsistent math delimiters when outputting formulas (such as mixing up single $ symbols with double $$ blocks or escaping brackets in ways that confuse compilers). The application pipes raw user text through a specialized regex pipeline to automatically clean and normalize these boundaries before passing them to the KaTeX renderer. It’s a perfect example of a modern application designed explicitly to work hand-in-hand with AI outputs.

The client-side logic was flawless, the editor was responsive, and the PDF printing engine worked natively and beautifully via an optimized @media print layout in styles.css. However, when this “vibe-coded” application was deployed to the cloud and subjected to an automated security audit, the results revealed a stark division between frontend aesthetics and infrastructure perimeter security.

Baseline Audit: Visually Perfect, Structurally Exposed

The initial external application security scan returned an alarming Overall Security Score of 50/100.

While the application achieved perfect marks for its TLS/SSL implementation (supporting only TLS 1.2+ with modern forward-secrecy cipher suites) and successfully locked down its environment files, it failed completely on the web network perimeter: HTTP Security Headers.

By default, standard serverless edge networks like Vercel serve raw web assets with extreme efficiency, but they do not attach explicit security rules to response envelopes unless explicitly instructed. The AI engine generated the React code flawlessly but left the deployment configuration entirely stock. This omission introduced several critical vulnerabilities:

Security Check Result Severity The Real-World Vulnerability & Exploit Vector
Content-Security-Policy (CSP) FAIL High Cross-Site Scripting (XSS): The browser had no instructions regarding which domains were trusted to load executable code. If an attacker managed to inject a malicious script via an input payload or a compromised third-party package, the browser would run it unconditionally, risking session hijacking or data leakage.
X-Frame-Options FAIL High Clickjacking Attacks: Lacking this header, any malicious third-party website could embed the Markdown-to-PDF tool inside an invisible HTML <iframe>. Attackers could overlay deceptive buttons directly over the actual editor, tricking users into clicking elements on the app without their knowledge.
X-Content-Type-Options FAIL Medium MIME-Type Sniffing: Without the nosniff directive, web browsers might ignore the declared Content-Type of an asset and attempt to guess its format based on its contents. An attacker could find a way to make the app fetch a malicious script disguised as a plain text or image file, which the browser would then execute.
Server Header Disclosure FAIL Medium Technology Stack Information Leakage: The server headers openly broadcasted Server: Vercel. While hosting on Vercel is highly secure, providing an explicit technological footprint helps malicious actors map your exact software stack and research tailored exploits.
Referrer & Permissions Policies FAIL Med/Low Information Leaks & Unrestricted Access: Outbound link clicks could leak internal URL query structures to external analytics platforms. Furthermore, the browser was not explicitly barred from accessing client hardware like the camera, microphone, or geolocation APIs.

The Vibe Coding Trap

AI models excel at what you show them in the viewport. They focus on the components, state hooks, and style layouts you interact with directly. They do not naturally consider the invisible, server-side HTTP handshake happening between the web browser and your cloud provider’s edge node. That oversight is exactly where vulnerabilities hide.

Infrastructure Hardening

To rectify these vulnerabilities, there is no need to modify or rewrite a single line of your React application logic. Instead, the hardening must be applied at the infrastructure configuration layer.

On serverless hosting networks like Vercel, this is accomplished by introducing a declarative configuration file—vercel.json—at the absolute root of your project repository. This configuration forces the cloud server’s edge nodes to inject explicit, protective headers into every outbound HTTP asset transmission.

Here is the exact production-ready configuration file deployed to seal the architectural gaps highlighted in the baseline audit:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        },
        {
          "key": "Referrer-Policy",
          "value": "strict-origin-when-cross-origin"
        },
        {
          "key": "Permissions-Policy",
          "value": "camera=(), microphone=(), geolocation=()"
        }
      ]
    }
  ]
}

Breaking Down the Security Injection:

  1. Content-Security-Policy (CSP): Rigorously confines asset execution. Declaring default-src 'self' prevents the browser from loading unauthorized remote scripts. It accommodates modern single-page frontend frameworks by safely allowing 'unsafe-inline' and 'unsafe-eval' (which are necessary for client-side routing, compilation, and KaTeX math processing), while guaranteeing that image boundaries (img-src) and network fetch destinations (connect-src) remain strictly enclosed.
  2. X-Frame-Options: DENY: Completely cuts off framing capabilities, neutralizing clickjacking threats instantly. No foreign origin can encapsulate this user interface.
  3. X-Content-Type-Options: nosniff: Forces the browser to strictly follow the MIME-types provided in the network response, shutting down content-type sniffing vectors.
  4. Permissions-Policy: Implements the Principle of Least Privilege at the browser hardware layer. By explicitly setting empty restrictions for camera=(), microphone=(), and geolocation=(), the application signals that it has no business requesting sensitive hardware permissions, establishing deep trust with privacy-conscious users.

The Post-Remediation Posture: 100% Perimeter Defended

Following the deployment of the root-level vercel.json file, a secondary automated compliance audit was triggered. The application’s Overall Security Score jumped from a failing 50 up to a highly secure 84/100, and the critical HTTP Security Headers category achieved a perfect 100/100 pass rate.

However, a closer look at the remaining minor scanner notifications provides a vital lesson in balancing academic compliance checklists with practical real-world application engineering.

The Security vs. Performance Tradeoff (Cache-Control)

The security scanner still flagged a warning on the Cache-Control header because it utilized a standard static caching layout (public, max-age=0, must-revalidate) instead of a strict no-store policy.

In a heavy, full-stack application dealing with financial ledgers, medical records, or user authentication sessions, no-store is a non-negotiable requirement to prevent shared proxies from caching private data.

However, this Markdown-to-PDF tool is a purely client-side utility. It features no backend servers, no user data tables, and transmits no private user tokens over the wire. For an open static frontend, enforcing a strict no-store policy would destroy application performance. Browsers would be barred from local asset caching, resulting in sluggish load speeds, higher data consumption, and increased rendering latency on every single refresh—offering absolutely zero security benefit in return. Maintaining standard static caching was an intentional, correct engineering tradeoff.

Filtering Compliance Noise

Automated security tools evaluate applications against generic compliance matrices. The secondary report noted several informational flags under categories like Database Security (demanding parameterized queries and VPC network separation) and Credential Handling (requiring Argon2 password hashing and session invalidation middleware).

Because our architecture has no database tier, no login forms, and no stateful backend sessions, these metrics are completely non-applicable. Securing an AI-coded application means understanding your architecture well enough to separate critical, exploitable perimeter vulnerabilities from automated compliance noise that does not apply to your stack.

The AI Coder’s Actionable Security Checklist

If you are building and deploying software using AI orchestration tools, you must add an independent validation phase to your deployment loop. Use this four-step checklist to secure your applications before pushing them live:

  • [ ] Harden the Edge Hosting Layer: Never assume your cloud provider’s default state is production-ready. If deploying a frontend application to Vercel, Netlify, or Cloudflare Pages, always include a dedicated configuration file (such as vercel.json or _headers) to explicitly inject security headers (CSP, X-Frame-Options, X-Content-Type-Options).
  • [ ] Sanitize and Control Untrusted Inputs: AI-generated outputs can be unpredictable. If your application acts as a downstream consumer of LLM tokens (such as an editor digesting copy-pasted or API-piped text), implement structural sanitization steps—like the custom regex “AI Bracket Fix” to clean equations, or a library like DOMPurify to scrub HTML before rendering it inside the browser.
  • [ ] Minimize Information Leakage: Strip verbose architecture details from your deployment metadata. Suppress or modify response headers that disclose underlying platforms (like Server or X-Powered-By) to avoid handing attackers an exact map of your technology stack.
  • [ ] Audit Reports with Common Sense: Use automated scanning tools to expose genuine architectural gaps, but verify their findings against the reality of your application’s design. Do not degrade application load times or break framework performance metrics to check a box for a database compliance rule if your application runs entirely on the client side.

Conclusion

AI allows us to write and deploy code at an extraordinary pace, but it does not relieve us of our responsibility to protect our users. “Vibe coding” is a phenomenal mechanism for rapid feature prototyping and creative execution, but the final architectural hardening phase still requires human engineering oversight. Spend ten minutes auditing your infrastructure files, secure your network handshakes, and ensure that your lightning-fast deployments are as safe as they are beautiful.

Build fast, but build secure.

Beyond Runtime: SkillLite’s Full-Chain Security for Evolving Agents

Beyond Runtime: SkillLite’s Full-Chain Security for Evolving Agents

Developing AI agents that can learn and adapt is exciting, but it introduces a critical security challenge: how do you safely allow an agent to evolve its own code, prompts, or tools without opening the door to vulnerabilities? Traditional sandbox solutions often focus on runtime isolation, leaving significant gaps in the agent’s lifecycle.

This is where SkillLite offers a different approach. Instead of solely relying on runtime containment, SkillLite implements a “full-chain” security model designed to protect self-evolving agents from installation through execution, even as they generate new behaviors.

What SkillLite is optimized for

SkillLite is a lightweight, Rust-native engine built for secure, local-first AI agent execution and evolution. Its core optimization is enabling agents to self-improve (evolve prompts, memory, and skills) while maintaining a high security bar across the entire lifecycle. It achieves this with:

  • Native system-level sandboxing: Leveraging OS-specific isolation mechanisms like Seatbelt (macOS) or bwrap/seccomp (Linux).
  • Zero dependencies: The core binary is self-contained, simplifying deployment and reducing supply-chain risk.
  • Fully local execution: Designed to run offline, without requiring external cloud services for core functionality.
  • Full-chain security: A multi-layered defense that covers install-time, pre-execution, and runtime phases.

The project explicitly states its goal: “the real value isn’t just safe execution — it’s safe evolution.” This means it’s built for scenarios where agents dynamically generate or modify their operational logic, and you need assurance that these changes are vetted before they run.

Common alternatives or approaches

When developers need to isolate untrusted code, several common solutions come to mind, each with its own strengths and typical use cases:

  • Docker: A widely adopted containerization platform that provides process and filesystem isolation. Docker containers package applications and their dependencies, offering a consistent runtime environment. It’s excellent for deploying microservices, CI/CD pipelines, and ensuring environment parity.
  • Pyodide: A Python distribution compiled to WebAssembly, allowing Python code to run directly in the browser. Pyodide is ideal for interactive web applications, educational tools, and scenarios where Python logic needs to execute client-side within the browser’s sandbox.
  • Other agent-specific sandboxes (e.g., E2B, Claude SRT): These are often specialized environments provided by AI platforms or frameworks, offering varying degrees of isolation and capabilities tailored for agent execution.

While these solutions provide a form of sandboxing, their security models and focus areas differ significantly from SkillLite’s full-chain approach, especially concerning the lifecycle of evolving code.

Where SkillLite stands out

SkillLite’s primary differentiator is its comprehensive, multi-layered security architecture, which it terms “Full-Chain Defense.” Unlike many solutions that focus predominantly on runtime isolation, SkillLite integrates security checks throughout the agent’s lifecycle.

Here’s how SkillLite’s security layers work:

flowchart TD
    A[Agent proposes new Skill/Prompt/Memory] --> B{Evolution Engine};
    B --> C[Evolved Artifact (e.g., Python script)];

    subgraph Full-Chain Security Defense
        C --> D1[Layer 1: Install-time Scanning]
        D1 --> D2[Layer 2: Pre-execution Authorization]
        D2 --> D3[Layer 3: Runtime Sandbox]
    end

    D1 -- Static rule scan --> D1_1[Regex pattern matching]
    D1 -- LLM-assisted analysis --> D1_2[Suspicious code confirmation]
    D1 -- Supply-chain audit --> D1_3[PyPI / OSV vuln DB check]

    D2 -- Two-phase confirm --> D2_1[Scan results -> User OK -> Run]
    D2 -- Integrity check --> D2_2[Hash tamper detection]

    D3 -- OS-native isolation --> D3_1[Seatbelt / bwrap / seccomp]
    D3 -- Process-exec whitelist --> D3_2[Interpreter only]
    D3 -- Filesystem / network / IPC lockdown --> D3_3[Restricted access]
    D3 -- Resource limits --> D3_4[CPU / mem / fork / fsize]

    D3 --> E{Execution Environment};
    E -- If all layers pass --> F[Skill executed safely];
    E -- If any layer fails --> G[Execution blocked / Rollback];
  1. Layer 1 — Install-time Scanning: Before any skill or artifact is even considered for execution, SkillLite performs static analysis, leverages LLM-assisted checks for suspicious patterns, and audits against supply-chain vulnerability databases (like PyPI or OSV). This proactive scanning aims to catch issues before they can even enter the system.
  2. Layer 2 — Pre-execution Authorization: Once an artifact passes install-time checks, it undergoes a two-phase confirmation process, often requiring user approval. An integrity check (hash tamper detection) ensures the artifact hasn’t been modified since its initial scan.
  3. Layer 3 — Runtime Sandbox: This is the familiar isolation layer, but with a heightened focus on native OS capabilities. SkillLite uses Seatbelt on macOS and bwrap or seccomp on Linux to enforce strict controls:
    • Process-exec whitelist: Only explicitly allowed interpreters (e.g., Python) can run, preventing arbitrary binary execution.
    • Filesystem, network, and IPC lockdown: Restricts access to sensitive system resources.
    • Resource limits: Prevents denial-of-service attacks by capping CPU, memory, fork, and file size usage.

This layered approach is particularly relevant for self-evolving agents. Every new prompt, memory pattern, or generated skill, even if created by the agent itself, must pass through these same rigorous checks. This ensures that the agent’s evolution doesn’t inadvertently introduce new security risks.

Comparing directly with Docker and Pyodide, SkillLite’s README.md highlights several areas of strength:

Capability SkillLite Docker (default) Pyodide
Install-time scanning ✅ — —
Static code analysis ✅ — —
Supply-chain audit ✅ — —
Process-exec whitelist ✅ — —
IPC / kernel lockdown ✅ — —
Filesystem isolation ✅ partial ✅
Network isolation ✅ — ✅
Resource limits ✅ partial partial
Runtime sandbox ✅ ✅ ✅
Zero-dependency install ✅ — —
Offline capable ✅ partial ✅

The project’s own 20-item security test suite shows SkillLite blocking all 20 items, achieving a 100% score, compared to 10% for Docker (default) and 35% for Pyodide. This suggests a more stringent default security posture, especially in areas like process execution, network access, and resource limits.

Trade-offs

While SkillLite’s security model is compelling, especially for evolving agents, it comes with its own set of trade-offs:

  • Scope: SkillLite is purpose-built for secure agent execution and evolution. If your primary need is general-purpose application deployment, CI/CD, or microservices orchestration, Docker’s broader ecosystem, tooling, and community support might be a more natural fit.
  • Ecosystem Integration: Being a Rust-native binary, integrating SkillLite into existing Python-heavy or JavaScript-heavy workflows might require using its Python SDK or CLI, rather than directly leveraging language-native sandboxing primitives. Docker, by contrast, is language-agnostic at the container level.
  • Browser Execution: Pyodide’s strength lies in bringing Python to the browser. SkillLite, being a system-level sandbox, is not designed for client-side web execution.
  • Flexibility vs. Security: The strictness of SkillLite’s sandbox (e.g., process-exec whitelist, full network lockdown by default) means that if an agent legitimately needs broader system access or network communication, these permissions must be explicitly configured and managed, potentially adding complexity. Docker, by default, is more permissive and requires explicit hardening.

Decision guide

Consider SkillLite when:

  • You are building self-evolving AI agents: If your agents generate or modify their own prompts, memory, or skills, SkillLite’s full-chain security model provides critical assurance that these evolved artifacts are vetted before execution.
  • Security is paramount for untrusted code execution: For scenarios where executing potentially malicious or buggy agent-generated code poses a high risk, SkillLite’s layered defense offers a higher default security posture than many general-purpose sandboxes.
  • You need local, offline, and zero-dependency execution: Its Rust-native, self-contained binary is ideal for edge devices, air-gapped environments, or applications requiring minimal runtime overhead and external dependencies.
  • You want to integrate a secure sandbox into an existing agent framework: The skilllite-sandbox binary can be used as a standalone component, allowing other agent frameworks to leverage its isolation capabilities without adopting the full SkillLite stack.

You might prefer alternatives like Docker or Pyodide if:

  • Your primary need is general application containerization or CI/CD: Docker’s ecosystem is unmatched for deploying and managing diverse applications in server environments.
  • You need to run Python code directly in a web browser: Pyodide is the go-to for client-side Python execution in web applications.
  • Your agents require broad, unconstrained system or network access by design: While SkillLite can be configured, its default posture is highly restrictive, which might be cumbersome if your use case inherently demands more open permissions.
  • You already have a mature security and isolation strategy in place: If your existing infrastructure already provides robust multi-layered security (e.g., VM-based isolation, highly hardened containers), the additional benefits of SkillLite might be less pronounced.

Migration or adoption notes

SkillLite offers a few entry points for adoption:

  • Full Stack: For new projects or those looking to leverage SkillLite’s agent evolution capabilities, the skilllite CLI and Python SDK provide a complete solution. The Python SDK allows Python developers to interact with the Rust-native engine.
  • Sandbox Only: If you have an existing agent framework and primarily need a robust, lightweight sandbox, you can integrate the skilllite-sandbox binary. This allows you to leverage SkillLite’s runtime isolation without adopting its agent evolution engine.
  • Desktop GUI: For local assistant use cases, a desktop GUI is also available, providing a user-friendly interface for managing skills and agents.

Given its Rust foundation, developers comfortable with Rust can extend or customize SkillLite directly. Python developers can integrate via the provided SDK, abstracting away the Rust implementation details.

Unsupported assumptions to verify

Before committing to SkillLite, verify the following based on your specific environment and requirements:

  • OS-native sandbox compatibility: While SkillLite leverages Seatbelt (macOS) and bwrap/seccomp (Linux), the exact behavior and compatibility can vary across different OS versions and distributions. Verify its performance and stability on your target production OS.
  • Specific resource limits: The README.md mentions resource limits (CPU/mem/fork/fsize). Confirm if the default limits or configurable options meet your agent’s expected resource consumption without causing unintended throttling or failures.
  • Network access requirements: If your agents require specific outbound network access (e.g., to external APIs), understand how to configure the network isolation to allow only necessary connections while maintaining security. The default is highly restrictive.
  • LLM-assisted analysis efficacy: The “LLM-assisted analysis” for install-time scanning is a novel approach. Understand its current capabilities, false positive/negative rates, and how it integrates with your trust model for new skills.
  • Python SDK feature parity: If you plan to use the Python SDK, ensure it exposes all the necessary functionalities of the underlying Rust engine for your specific use case.

Context-Driven Testing: What It Is and Why It Matters Now

Context-driven testing rejects the notion that there’s a universal “best practice” for QA. Instead, it holds that the value of any testing approach depends entirely on the context you’re operating in. What works at a regulated bank with 50 QA engineers is actively harmful at a startup with one contractor and no formal QA process.

And right now, that distinction matters more than ever.

AI is creating a false sense of coverage. Teams are generating thousands of test cases with LLMs and mistaking volume for coverage.
Without proper context, you end up with beautifully written test cases that miss the actual risk entirely.

In startup environments especially: apply heavyweight process to every feature and you’ll be ignored. Test everything equally and you’ll miss what actually matters to your customers.

Imagine a scenario where you have limited institutional power, the company you work for has customers with high ARR who can escalate over your head, and has no established QA process to lean on. Context driven testing gives you a better vision for every decision: rather than just testing the feature, you can ask things like “What’s the actual risk if it breaks?” or “Who is most likely to be harmed if we miss this?”. Those two questions do more work than any test plan template.

Context driven testing is not an excuse to do less testing, it’s actually the opposite, since this is way harder, it requires to develop critical thinking and really understanding the user needs and areas with greater risks, not just tooling.

Do you apply context-driven thinking in your daily work? What’s the highest-risk area in your system right now?

Building a Global Digital Store in a Country that Hates Cryptocurrency

The Problem We Were Actually Solving

When I started working on a platform for selling digital downloads, I thought the biggest challenge would be getting the product right. I mean, it’s just a collection of files, how hard could it be? But what I quickly realized was that our biggest problem was actually accessing the global market. Our team was based in Iran, where the government has strict restrictions on cryptocurrency usage and international transactions. Getting paid through traditional payment gateways like Stripe or PayPal was out of the question. We were stuck with a product and no way to sell it.

What We Tried First (And Why It Failed)

Our first attempt was to use a non-crypto-based payment gateway that accepted Iranian credit cards. Sounds like a straightforward solution, right? Wrong. The processing fees were astronomical, and the gateway would often freeze our funds for no reason, citing “anti-money laundering” regulations. We lost hundreds of dollars in transaction fees and had to spend weeks disputing the charges. Meanwhile, our customers were left waiting for their orders, which only added to our reputation problems. It was a disaster.

The Architecture Decision

After months of struggling with payment gateways, we decided to take a different approach. We chose to implement a cryptocurrency-based payment system, specifically Bitcoin, but with a crucial twist. We partnered with a few major cryptocurrency exchanges in the region, allowing us to accept a variety of cryptocurrencies, not just Bitcoin. This gave our customers more options and made it easier for them to buy from us, without having to convert their funds into fiat currency. We also implemented advanced AML/KYC checks to meet the requirements of these exchanges. We were willing to take on the added complexity, if it meant we could access the global market.

What The Numbers Said After

The switch to cryptocurrency payments was a game-changer for us. We saw a significant increase in sales and a major reduction in transaction fees. Our revenue grew by over 300% in the first quarter alone, and our customer satisfaction ratings soared. The best part? We were able to compete with global marketplaces, bypassing the traditional payment gateways that had been holding us back. Our sales continued to grow, and we were able to expand our team, hire more developers, and improve our product offerings.

What I Would Do Differently

If I were to do it all over again, I would focus on building a more robust fraud detection and prevention system from the start. Our AML/KYC checks were great, but they weren’t foolproof. We had to manually review and verify many transactions, which added to our workload. Additionally, I would consider implementing a more advanced invoice and order management system, to streamline our fulfillment process and reduce errors. These are just minor suggestions, but they would have made a significant impact on our operational efficiency. In the end, we learned that being a successful digital store in a restricted country requires a willingness to take on complexity and push the boundaries of what’s possible.