Mischa Sigtermans

Thoughts
· Open Source AI

Why I built Laravel TOON (and how it saves 60% on LLM tokens)

I was burning tokens on nested JSON in Stagent. Existing TOON packages didn't flatten nested objects. So I built Laravel TOON. Here's how it saves 40-60%.

When you're building MCP servers or sending data to LLMs, every token counts. JSON is verbose. Repeated keys, curly braces, quotes, colons. For a 50-record response, you're burning thousands of tokens on structure alone.

I ran into this problem building an MCP server for Stagent, a booking platform for music agencies. The data is gnarly: thousands of bookings, each with nested event details, venue info, artist data, financial terms. I looked at existing TOON packages for Laravel, but none handled nested objects properly. They'd keep the nesting intact, missing the biggest optimization opportunity.

So I built Laravel TOON.

The problem with nested data

A typical Stagent booking looks like this:

{
    "id": "p6YZkB4m",
    "status": "confirmed",
    "artist": {
        "id": "PGreBnRL",
        "name": "René Bourgeois"
    },
    "event": {
        "id": "3G5wmW4X",
        "name": "Jetset Kidz",
        "status": "completed",
        "type": null
    },
    "venue": {
        "name": "Aramon",
        "city": null,
        "country": null
    },
    "buyer": {
        "id": "8GOePv49",
        "name": "Jet Set Seven Gmbh"
    },
    "starts_at": "2013-06-18"
}

That's a lot of repeated structure when you have hundreds of bookings. Every "artist":, every "event":, every curly brace, they all consume tokens. Multiply that across 50 or 100 records and you're wasting context window space that could hold actual data.

The opportunity was clear: flatten nested objects into column headers, turn records into rows.

What Laravel TOON does differently

TOON (Token-Optimized Object Notation) is a compact, YAML-like format. The key feature of my implementation is intelligent nested object flattening. Instead of preserving nested structures, it flattens them using dot notation in the header:

$bookings = [
    [
        'id' => 'abc123',
        'artist' => ['name' => 'Amelie Lens', 'id' => 'art1'],
        'event' => [
            'name' => 'Awakenings',
            'venue' => ['city' => 'Amsterdam', 'country' => 'NL']
        ],
        'fee' => 15000,
    ],
    [
        'id' => 'def456',
        'artist' => ['name' => 'Charlotte de Witte', 'id' => 'art2'],
        'event' => [
            'name' => 'Tomorrowland',
            'venue' => ['city' => 'Boom', 'country' => 'BE']
        ],
        'fee' => 25000,
    ],
];

Toon::encode($bookings);

Output:

items[2]{id,artist.name,artist.id,event.name,event.venue.city,event.venue.country,fee}:
  abc123,Amelie Lens,art1,Awakenings,Amsterdam,NL,15000
  def456,Charlotte de Witte,art2,Tomorrowland,Boom,BE,25000

The nested event.venue.city and artist.name paths get flattened into column headers. Values become rows. No repeated keys. No braces. No quotes around strings that don't need them.

This is what other Laravel TOON packages miss. They encode TOON syntax but keep nested objects intact, leaving significant optimization on the table.

Real-world benchmarks

I tested this against production data from Stagent with 17.000+ bookings:

Records JSON Tokens TOON Tokens Saved Savings
10 2,868 1,223 1,645 57.4%
25 7,355 3,002 4,353 59.2%
50 14,720 5,924 8,796 59.8%
100 30,316 12,723 17,593 58.0%

For a typical paginated response of 50 records, that's roughly 2,200 tokens saved per request. When you're building AI features that make frequent database queries, those savings compound fast.

Configuration options

The package includes options to squeeze out extra savings:

// config/toon.php
return [
    // Omit null, empty strings, or false values
    'omit' => ['null', 'empty'],

    // Strip specific keys entirely
    'omit_keys' => ['created_at', 'updated_at', 'deleted_at'],

    // Shorten verbose keys
    'key_aliases' => [
        'organization_id' => 'org_id',
        'description' => 'desc',
    ],

    // Truncate long strings
    'truncate_strings' => 200,

    // Control nesting depth (default: 3)
    'max_flatten_depth' => 3,
];

Omitting timestamps and using key aliases alone can shave off another 10-15% depending on your data structure.

Use cases

MCP servers

If you're building MCP servers with Laravel, TOON is a natural fit. Tool responses often return database records, and those add up fast. Wrapping your tool responses in Toon::encode() means the AI gets the same data with fewer tokens:

use MischaSigtermans\Toon\Facades\Toon;

class ListBookings extends Tool
{
    public function handle(Request $request): Response
    {
        $bookings = Booking::query()
            ->with(['event.venue', 'artist', 'event.buyer'])
            ->limit(50)
            ->get()
            ->map(fn ($b) => $this->formatBooking($b));

        return Response::text(Toon::encode([
            'count' => $bookings->count(),
            'bookings' => $bookings,
        ]));
    }
}

This leaves more room in the context window for reasoning instead of burning it on data structure.

LLM context packing

When you need to include data in prompts, every token matters:

$context = Toon::encode([
    'user' => $user->only(['id', 'name', 'role']),
    'recent_bookings' => $bookings,
    'stats' => $statistics,
]);

$response = $llm->chat("Analyze this data: {$context}");

Measuring your savings

Want to see exactly what you're saving? Use the diff method:

$diff = Toon::diff($data);

// Returns:
// [
//     'json_chars' => 14720,
//     'toon_chars' => 5924,
//     'saved_chars' => 8796,
//     'savings_percent' => 59.8,
// ]

This helps when you're deciding whether TOON makes sense for a particular data shape.

When TOON works best

TOON shines for uniform arrays of objects, the kind of data you get from database queries. Multiple records with the same fields, consistent structure across items. That's where you see 50-60% savings.

For deeply nested, non-uniform data, the benefits decrease. And for single objects rather than arrays, JSON might be just as efficient. The diff() method helps you measure before committing.

Installation

composer require mischasigtermans/laravel-toon

Full round-trip fidelity. Decode gets you the exact original structure back, types preserved:

$original = Toon::decode($encoded);

What I learned building this

Building Laravel TOON reinforced something I keep learning: the best solutions come from real problems. I wasn't trying to build a package. I was trying to make my MCP server responses fit in the context window.

When I evaluated existing packages and found they didn't handle nested flattening, I had two choices: work around the limitation or fix it. Since joining Ryde Ventures and working on AI products daily, I've learned that these small optimizations matter. Token costs add up. Context windows fill up. The tools that save you 60% on every request earn their place in the stack.

The package is open source on GitHub. If you're building AI features in Laravel and hitting token limits, give it a try. And if you find edge cases or have ideas for improvement, I'd love to hear from you.

thanks for reading

Hi, I'm Mischa. I've been Shipping products and building ventures for over a decade. First exit at 25, second at 30. Now Partner & CPO at Ryde Ventures, an AI venture studio in Amsterdam. Currently shipping Stagent and Onoma. Based in Hong Kong. I write about what I learn along the way.

Keep reading: Stagent and Artwin join forces for the future of bookings.

Thoughts