This is the documentation of version 1.0.0-rc.4. Go to the latest version, 1.0.0-rc.5.

Securing your agent

An agent application has two keys, not one. The Security guides cover the TomTom API key. This one covers the other, which is the key to your model provider and the bill attached to it.

They deserve separate treatment because the two keys cost differently. Map calls are billed at a published rate you can reason about. Model calls are billed by how much text goes through them, which makes an unattended provider key harder to put a number on.

Where the agent runs

All of it runs in the browser. ToolState needs a live MapLibre instance, so the agent loop, the model call and every tool execution happen on the page in front of the user.

Browser (everything here is editable by the user)Your proxy (holds both keys)Model providerapi.tomtom.comAgent loop (system prompt, maxSteps, tool schemas)Tool execution (map, search, routing) prompts + conversation historyservice calls model key added, model pinned, tokens cappedTomTom key added here

Everything inside the red boundary is visible and editable by the person using your application. That includes the system prompt, the tool definitions, the model name and any limit you set in JavaScript. Treat all of it as published.

As in the API key guide, and for the same reason, a provider key configured in the page is a public key. It is worse than the map key, because most model providers offer nothing like a referrer restriction. Once the key has left, nothing ties it to your domain.

Proxy the model too

The fix is the same as for proxying your API calls. A small service of yours holds the key and forwards the request.

AI SDK providers accept a baseURL, so you point the agent at your proxy when you create the provider, not the agent.

import { createOpenAI } from '@ai-sdk/openai';
import { createMapAgent } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';

const openai = createOpenAI({
    baseURL: 'https://your-app.example.com/api/model',
    // The provider requires some value here. Your proxy replaces the
    // Authorization header with the real key on the way out, so this is a
    // placeholder, never a working key.
    apiKey: 'proxied',
});

const agent = createMapAgent(map, {
    model: openai('gpt-4o'),
});

Run the four checks from the proxy hardening guide on this route too, unchanged. They are the origin allowlist, the challenge gate, the session cookie and the rate limit.

What a model route needs on top

Map requests all cost roughly the same, so a generous ceiling works for them. Model requests don't. One chat turn can cost a thousand times another, depending on how much text went into it, so set the limits on this route deliberately.

Four settings do most of the work, and all of them belong on the server, where the client cannot change them:

Then set a spend cap and a billing alert at the provider. It costs nothing and catches whatever the settings above did not anticipate.

Streaming responses make usage accounting arrive late. If you meter on the usage figures in the response, a burst of concurrent streams can overshoot the ceiling before any of them report. Cap concurrency per session as well as rate.

What the toolkit already bounds

The toolkit sets some limits itself. They hold with or without a proxy, because they constrain the loop, not the caller.

You can observe all of it through onToolExecute, which fires per call with the tool name, duration and error state:

const agent = createMapAgent(map, {
    model: openai('gpt-4o'),
    maxSteps: 6, // tighter than the default, for expensive tool sets
    onToolExecute: ({ toolName, durationMs, isError }) => {
        telemetry.record('agent.tool', { toolName, durationMs, isError });
    },
});

What the toolkit cannot bound

The list above stops at the edge of one turn, because that is the last point where the toolkit is the only thing running.

It does not bound how many turns someone starts, how many tabs they open or what they send once they know your endpoint. It cannot meter tokens, since the provider call is yours to configure and theirs to answer. Nor can it attribute usage to a user, since it has no notion of one.

All of that needs state that outlives the page, which means a server. Without one, the numbers above are sensible defaults, not enforced limits.

Prompt injection

Content that reaches the model context can carry instructions, and the model has no reliable way to tell yours from someone else's. In an agent application, that content arrives from more places than the chat box. It also comes from property values in bring-your-own-data, place names and addresses returned by search, and anything you retrieve from a documentation or knowledge service.

The toolkit strips string values from BYOD profiles for this reason. That is one source among several, and it is the only one we can address for you, since we do not see what your custom tools return.

Treat anything a tool brings back as data. Give custom tools narrow schemas and specific capabilities instead of broad ones, and keep credentials and internal detail out of the system prompt, which any user can extract. If a tool has a real-world effect, put the confirmation in your own code on the server, not in a prompt that asks the model to check first.

Things that look like solutions

Related guides