The dangerous version of seat-booking AI is one function called `bookSeats` with a secret behind it. The useful version is a narrow tool layer that knows what it may suggest, hold, and commit.
The unsafe tool
Here is a tool definition we see teams reach for first:
{
"name": "bookSeats",
"description": "Book seats for the user",
"parameters": {
"event": "string",
"seats": "array",
"price": "number"
}
} It looks convenient. It isn't. That single call asks the model to choose the event, the seat authority, the price and an irreversible action in one step, with no way for the host to inspect any of those decisions before money moves. A prompt injection, a stale chart, an unsupported preference or a retry after a network timeout all collapse into the same outcome here: a commercial bug that nobody approved. That is the risk.
An agent seat map API is safe only when each of those choices becomes its own typed, scoped operation, carrying its own side-effect permission and its own audit record, so that a reviewer can see exactly which step a failure came from. Nothing less holds up.
The safer design keeps the model useful but makes the boundaries boring:
buyer language
→ host-owned typed tool
→ validated event/quantity/preferences
→ SeatLayer API with a server-held secret
→ temporary hold
→ human/payment approval
→ host booking transition SeatLayer's public MCP is a separate chart-scoped Designer authoring surface. No transactional MCP is involved here. The tools below belong to the host application and call the documented seat API from a trusted server.
Start with ownership
Before writing a schema, decide which system owns each noun:
| Noun | Owner | Why it matters to an agent |
|---|---|---|
| Event catalogue and buyer account | Host application | The agent should not enumerate unrelated tenant data |
| Chart and event inventory | Seat-map service | The agent must ask the current event state, not a stale prompt snapshot |
| Hold | Seat API, referenced by host order/session | A hold expires and can conflict |
| Price | Published event chart/host commercial rules | Never trust a model-supplied amount |
| Payment | Host checkout/payment provider | The model should not receive payment credentials |
| Commercial order | Host application | It supplies the stable reference for retry-safe booking |
| Human approval | Host product/policy | The user confirms the seats and terms before commitment |
Turn language into typed constraints
“Find me two seats together” contains useful intent, but it is not a server request yet. The host should resolve it into a constrained object:
{
"eventId": "host-event-1842",
"quantity": 2,
"adjacency": "together",
"categoryKey": "stalls",
"currency": "USD"
} The agent must not invent eventId, choose an unapproved category, or use currency to override the event's published price. The host resolves the event to a trusted SeatLayer event key and maps buyer language such as “stalls” to a stable, allowlisted categoryKey before it calls the API.
If the user asks for “aisle seats” but the current chart/API does not expose an aisle attribute, the tool should say so and offer supported alternatives. A good agent is allowed to refuse an unrepresentable preference. Refusal is a feature.
An illustrative tool schema might look like this:
{
"name": "prepare_best_available_hold",
"description": "Find and temporarily hold supported seats for a host event",
"inputSchema": {
"type": "object",
"required": ["eventId", "quantity"],
"properties": {
"eventId": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1, "maximum": 6},
"categoryKey": {"type": "string"}
},
"additionalProperties": false
}
} This schema is the host tool contract, not the SeatLayer endpoint schema. The adapter maps quantity to the documented server field qty and forwards only supported stable identifiers such as categoryKey. The six-seat maximum is a host policy in the example; it is not a claim about SeatLayer's server maximum.
Separate tool permissions
Four permission classes are easier to audit than one “booking” permission.
| Class | Example | Side effect | Who can invoke it |
|---|---|---|---|
| Read | Resolve a host event; inspect a supported seat/hold state | None | Agent with an event-scoped session |
| Reversible reserve | Request a bounded best-available hold; release it | Temporary inventory change | Agent under quantity, expiry, and rate limits |
| Commercial prepare | Inspect authoritative hold items; create host checkout intent | Host order state | Host server, after user confirmation |
| Commit | Charge and book with the host order reference | Durable commercial/inventory change | Host policy plus explicit approval; not free-form agent output |
The best-available seats API is the relevant technical source for the reversible selection step. The server-side hold guide is the source for a backend-managed hold when there is no browser selection in the loop.
Add controls for the failure modes you actually have
| Failure mode | Control in the host adapter | What the agent is told |
|---|---|---|
| Prompt injection in an event description | Treat event text as data; never as tool instructions | Only the host event ID and supported fields are authoritative |
| Stale event or chart revision | Resolve the event immediately before the hold | “Inventory changed; I’m refreshing” rather than a guessed seat |
| Unsupported preference | Validate against the published capability set | Ask for a supported alternative |
| Replay after a timeout | Require a stable request/correlation ID | Report pending status until reconciliation finishes |
| Cross-tenant access attempt | Enforce tenant and event scope on the server | Refuse without revealing neighbouring inventory |
| Excessive quantity or rapid retries | Apply host limits and rate controls | Explain the limit and offer a smaller search |
We put these controls in code and policy, not in a system prompt alone. A prompt can explain why a tool should be used; it cannot enforce that the request belongs to the current tenant.
Find and hold, but do not silently book
We keep the adapter's reserve function small, returning a clear result:
type HoldSummary = {
holdId: string;
expiresAt: number;
items: Array<{
label: string;
quantity: number;
unitPrice: number;
currency: string;
}>;
};
async function prepareHold(input: ValidatedSeatRequest): Promise<HoldSummary> {
// The secret is read only here, on the host server.
// The exact API call and supported fields are frozen by the test-mode spike.
const result = await seatApi.bestAvailable({
eventKey: input.seatLayerEventKey,
qty: input.quantity,
categoryKey: input.categoryKey,
});
return {
holdId: result.holdId,
expiresAt: result.expiresAt,
items: result.items,
};
} The model should see a human-facing summary, not a secret, raw authorization header, or unrelated event inventory. The summary should include enough information for the user to confirm the choice: seat labels, section/tier if supported, authoritative total, currency, and expiry.
Do not let the model calculate that total from its own text. The API response is the authority for the hold's items and prices; the host's checkout remains the authority for payment.
Ask for human confirmation
The confirmation message is a product surface, not an afterthought:
I found two adjacent seats in Stalls, A-12 and A-13.
The current total is $74.00 USD. They are held until 19:42 UTC.
Confirm checkout, choose different seats, or release this hold? The user should be able to inspect the visual chart when seat location matters. A conversational summary is useful for discovery; it is not always the best seat-selection interface. Accessibility, large venues, group context, and “show me the view” requests may all favour the direct buyer map.
Keep the commit path outside the model
After confirmation, the host creates or resumes its checkout flow. The sequence is:
- confirm that the hold is still active;
- inspect current authoritative line items;
- create the host payment intent/order;
- charge through the host payment provider;
- book the hold with the stable host order identifier as
bookingRef; - retry an ambiguous booking result using the same reference;
- release or let the hold expire when payment fails.
The model can help communicate each state. It should not receive the account secret or become the only place where the order state exists.
Expiry, conflicts, and ambiguity
An agent workflow needs more than a happy-path tool call.
| State | Tool result | Agent/product response |
|---|---|---|
| Unsupported preference | Validation failure | Explain the limitation and ask for a supported choice |
| Event closed or sold out | Structured conflict | Offer another event or return the user to discovery |
| Requested seats unavailable | `409`/conflict result | Keep the conversation context, refresh the chart, and propose alternatives |
| Hold expired | Missing/expired hold | Ask whether to search again; do not present the old seats as reserved |
| User abandons checkout | Active hold | Offer release or let the documented expiry return inventory |
| Booking timeout | Unknown commit state | Put the host order into pending/reconciliation and retry the same reference |
| Duplicate webhook/tool delivery | Repeat event | Verify and ignore the duplicate after idempotent processing |
The model should not “solve” a conflict by changing the price, silently choosing a different event, or claiming the original seats are still held.
Persist an approval state, not a chat transcript
The minimum durable record is a host-side workflow object keyed by bookingRef:
search_requested → hold_prepared → awaiting_approval
↘ released/expired
awaiting_approval → payment_pending → booking_pending → booked
↘ reconciliation_required Store the hold ID, expiry, event scope, authoritative item snapshot, user approval timestamp, and the idempotency key used for payment/booking. Keep conversational text as an optional display log, not as the source of state. If the user opens the conversation on another device, the host can render the same pending approval and ask for a fresh confirmation without asking the model to infer what happened.
Expiry is a state transition. A background worker or the next user action should mark an expired hold as such and remove it from the approval card. It must not remain clickable with a stale “Confirm” button. If the provider reports an ambiguous booking result, freeze the workflow in reconciliation_required and resolve it against the same bookingRef.
Approval should be bound to the commercial facts the user saw. Record a digest or version of the hold item summary with the approval. If a reinspection changes a label, category, quantity, currency, or total, invalidate the approval and show a new card. The model may explain the difference; it may not decide that a five-dollar increase is “close enough.”
Release deserves its own reversible tool. It should accept only a hold already associated with the current host session or order and return a structured released/already-gone result. Do not expose a generic “release these labels” action to the model: a guessed label set can interfere with another buyer. When the user abandons checkout, the workflow can offer release immediately; if that call is ambiguous, expiry remains the correctness fallback and the UI should stop promising that the hold is active.
Finally, keep approval and payment separate. “Yes, those seats” authorizes the seat choice and shown terms; it does not give a language model access to stored payment credentials. The host checkout applies its normal authentication, fraud, consent, and payment rules before the inventory commit. If checkout requires a step-up challenge or account reauthentication, the workflow should pause there without extending the hold unless the documented policy explicitly permits it.
Observe the tool without leaking the system
Record enough to debug a bad tool call:
- host user/session and authorised event scope;
- tool name, schema version, validation result, and correlation ID;
- SeatLayer request ID/status without the secret;
- hold ID and expiry only when the retention policy permits it;
- approval/order state and retry reference;
- redacted error and final user-facing result.
Never log the secret, raw model context containing it, payment credentials, or an unrestricted seat inventory dump. We treat this as the line that decides whether an agent transcript is safe to keep. Scope the agent session to the event/product context it needs.
Prove the tool layer before you wire it to a workflow
A tool contract is easy to describe and easy to get subtly wrong. Before this adapter goes near a production workflow, run it against a test-mode provider that exercises:
- a valid request for two adjacent seats;
- a rejected unsupported preference;
- a second request that receives a normal conflict;
- a hold expiry and refreshed search;
- a user confirmation summary based on authoritative items;
- a synthetic host approval before any commit call;
- a timeout retry using the same order reference;
- redacted audit output.
We keep the adapter and its event labelled as a teaching fixture. A single green test run isn't a production availability claim, and in our experience that conflation is what puts an agent in front of real inventory a sprint too early.
Capture a compact trace for each fixture: validated input, tool decision, redacted provider response, state transition, and user-facing message. A trace should make it obvious whether a failure happened before the provider call, during a normal inventory conflict, or after a commit whose result needs reconciliation. This matters when a retry is initiated by a workflow engine rather than the original HTTP request.
What the exercised adapter caught
Our fixture runs five decisions in order. First, a request containing an unlisted aisle field is rejected by additionalProperties: false behavior before any provider call. Second, a tenant-scoped host event resolves to the synthetic SeatLayer event key; an event outside that scope returns no neighbouring inventory. Third, the adapter prepares two adjacent synthetic labels and returns only holdId, expiry, authoritative item summaries, and requiresApproval: true.
The first commit attempt fails because no matching approval exists. After the host inserts an approval tied to the same hold, the booking succeeds with order_fixture_42. Repeating that logical booking returns the same result. The redacted audit contains the tool name, host event ID, hold ID, outcome, and booking reference—no secret, authorization header, buyer record, or unrestricted event dump.
We think that is useful proof, but its scope has to stay visible. The provider in the fixture is deterministic and in memory. It verifies the host tool's validation, approval, idempotency, and redaction behavior; it does not verify SeatLayer latency or live inventory. A credentialed test-event run is the final integration gate before the adapter is attached to a production workflow. Run that gate with an event-scoped test catalogue, never a live onsale.
Where Aerostack fits
Aerostack's value is the tool-hosting and workflow layer: typed tools, scoped connections, approval steps, and observable agent execution. SeatLayer supplies the documented seating/inventory operations. The host application owns the commercial boundary.
In an Aerostack-style workflow, keep the provider adapter behind one connection boundary and expose small actions to the workflow: resolve_event, prepare_hold, summarize_hold, request_approval, and release_hold. The names are illustrative; the important property is that each action has a single side-effect profile and an explicit input/output contract. A workflow can require an approval node before the host's commit_booking action and route conflicts to a refresh branch.
The approval node should display the same authoritative labels, tier, total, and expiry returned by the adapter. If a later step re-fetches the hold and the items differ, invalidate the approval and ask the user again. Never preserve a green approval check across a material change in price or seat identity.
That separation is good engineering. It also gives you an honest answer to “can an agent book seats?” — an agent can help a host prepare a safe hold and guide a user through a controlled checkout, while the host keeps authority over payment and durable booking.
When chat is the wrong interface
Do not put an agent in front of every seat selection. Use the visual picker when:
- location relative to a stage or exit matters;
- a group needs to compare several layouts;
- accessibility requires a direct, inspectable surface;
- the user wants to explore a large venue rather than state a preference;
- a hold has expired and the user should see current inventory.
Use agent tools for discovery, supported preference translation, best-available suggestions, and clear handoff into the visual/checkout flow.
The safe pattern
Do not give a model a booking key and ask it to “try its best.” Give the model narrow, typed, scoped tools. Let a reversible hold be visible and expire. Require the host's payment/order policy before commit. Keep the chart available as the visual source of truth.
That pattern is less magical than a one-function demo. It is also something an agent platform, ticketing product, and seat-map API can each own without pretending the boundaries do not exist.
Evidence note
We wrote the host adapter and its focused test with Node's strict assertions, synthetic event/order identifiers, and a deterministic in-memory provider. The two linked SeatLayer pages are the sources for the current best-available and server-hold contracts. Recheck those contracts and run your own adapter against a scoped test event before you trust it — nothing here is production telemetry.
Agent seat-booking tools: direct answers
Can an AI agent actually book seats for a user?
[object Object]
Why not just expose one bookSeats tool to the model?
[object Object]
Is this the same as SeatLayer's MCP?
[object Object]
How do you stop a retry from double-booking?
[object Object]
What should never appear in the agent's context or logs?
[object Object]