DEV Community

jamilxt
jamilxt

Posted on

DoorDash Has Seen Hundreds of AI-Agent Orders. Build a Checkout That Knows When to Stop

A shopping agent can find a restaurant, assemble a cart, and call an ordering API. The hard question comes one step later: when is it allowed to spend the user's money?

DoorDash's latest earnings discussion offers a useful reality check. CEO Tony Xu said the company had seen “maybe several hundred orders in total” from some third-party AI agents. That is a cumulative count for those agents, not all orders and not a quarterly total. The same report says customers still predominantly order through DoorDash's own app. The company has also built its own agent experiences and an MCP connector for corporate ordering. The signal is not that agents cannot order. It is that connecting an agent to a real fulfillment network does not automatically make people use it. TechCrunch reported Xu's remarks and context.[1] The SEC-filed transcript of DoorDash's Q3 earnings call records Xu's cumulative figure and describes its broader strategy around digital assistants and physical fulfillment.[2]

For developers, the practical lesson is smaller and more actionable: do not let a language model turn a suggestion into an irreversible purchase. Build a deterministic boundary between “the agent found an option” and “the merchant accepted an order.” This tutorial develops that boundary as a small checkout state machine. It is a teaching example, not a production payment integration.

The order should be data before it becomes an action

A conversation is a poor source of authority. “Get the usual, but cheaper” is not a payment authorization. It leaves open which items, substitutions, delivery address, tip, fees, and final amount the user accepted.

The agent can help interpret intent and propose a basket. The application should then turn that proposal into a structured quote from the merchant. The quote is the object the user reviews. It should include exact line items, quantities, merchant, address, total, currency, and an expiration time. If inventory or price changes, create a new quote and require a new decision rather than silently editing the old one.

This is not merely a prudent UI convention. The Agent Payments Protocol (AP2) specification separates the shopping agent, merchant, credential provider, payment processor, and a trusted surface for user consent. It says transaction validation must happen in deterministic code. Its direct flow has a person approve the closed checkout and payment, while its autonomous flow uses user-approved constraints that the completed checkout must satisfy. Those are different authorization modes, not interchangeable versions of a model prompt.[3]

A compact state machine

Start with explicit states:

  • DRAFT: the agent is still assembling a proposed order.
  • QUOTED: the merchant has returned a concrete, expiring quote.
  • AWAITING_APPROVAL: the user is shown that exact quote.
  • APPROVED: the user approved the quote, and the approval is bound to its identifier and amount.
  • SUBMITTING: the server is making one idempotent order request.
  • CONFIRMED: the merchant accepted it and returned an order identifier.
  • REJECTED, EXPIRED, or FAILED: a terminal result that cannot be mistaken for success.

The key invariant is simple: no transition from DRAFT directly to SUBMITTING. The server checks the quote, the approval, and the merchant's current terms first. A model may request a transition, but it cannot grant itself the permission.

Here is a TypeScript sketch of the policy check. It deliberately stops before payment processing. Production systems should use the merchant and payment provider's supported APIs rather than treating this function as a substitute for them.

type Quote = {
  id: string;
  userId: string;
  merchantId: string;
  currency: string;
  totalMinor: number;
  expiresAt: number;
  items: Array<{ sku: string; quantity: number }>;
};

type Approval = {
  quoteId: string;
  userId: string;
  approvedAt: number;
  maxTotalMinor: number;
};

function canSubmit(
  quote: Quote,
  approval: Approval | undefined,
  now: number,
): boolean {
  if (!approval) return false;
  if (approval.userId !== quote.userId) return false;
  if (approval.quoteId !== quote.id) return false;
  if (now >= quote.expiresAt) return false;
  if (quote.totalMinor > approval.maxTotalMinor) return false;
  if (quote.items.length === 0) return false;
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Use integer minor units for money, such as cents, not floating-point dollars. The approval should refer to the immutable quote ID, not just “approved: true.” That makes a changed basket a different transaction. In a real service, the quote and approval records belong in durable storage with a transaction or compare-and-set transition so two workers cannot submit the same order concurrently.

Make retries safe, not merely convenient

Networks fail at awkward moments. Your server may send an order request, the merchant may accept it, and the response may time out before your application receives it. Retrying with a new request can create a duplicate dinner order.

Generate an idempotency key from the approved quote and persist it before contacting the merchant. Reuse that same key for every retry of the same logical submission. Record the request state, merchant response, and returned order ID. If the provider does not support idempotency, use its documented lookup or reconciliation mechanism before retrying. Do not invent a new order just because the first response was lost.

The state transition should be guarded server-side, roughly like this:

async function submitApprovedQuote(quoteId: string, userId: string) {
  const attempt = await db.transaction(async tx => {
    const order = await tx.orders.lockByQuoteId(quoteId);
    if (!order || order.userId !== userId) throw new Error("not found");
    if (order.status === "CONFIRMED") return { existing: order };
    if (order.status !== "APPROVED") throw new Error("not approved");
    if (order.expiresAt <= Date.now()) throw new Error("quote expired");
    return tx.orders.markSubmitting(order.id, order.idempotencyKey);
  });

  if ("existing" in attempt) return attempt.existing;
  return merchantApi.createOrder(attempt.payload, {
    idempotencyKey: attempt.idempotencyKey,
  });
}
Enter fullscreen mode Exit fullscreen mode

This snippet assumes a transaction, row lock, and merchant API behavior that your actual stack must implement and test. It is not enough to put an idempotency header in a request if your application forgets which key belongs to which approved quote.

Approval must be meaningful

A confirmation screen is not meaningful if it hides the details that could change the decision. Show the merchant, exact items and substitutions, delivery destination, fees, tax, tip, total, and the deadline for the quote. Make the action label explicit: “Place this order for $X” is clearer than “Continue.” If the amount rises, an item is substituted, or the address changes, return to approval.

The authorization must also have a scope. “Spend up to $40 at this merchant for these listed items before 7:15 PM” is narrower than “buy groceries for me.” The AP2 specification describes open mandates for autonomous purchases, where a user approves constraints and the agent produces a specific checkout that is evaluated against them. It also recommends short expirations and requires that an agent not reuse an open mandate for another checkout until the previous one is resolved. That is a useful design pattern even if your implementation does not adopt AP2: bind permission to limits, merchant, duration, and the transaction being made.[3]

Payment credentials need similar boundaries. Stripe's agentic commerce guide describes Shared Payment Tokens scoped to a retailer, order amount, and time window, so an agent can initiate a payment without seeing the underlying payment details. That is a vendor-specific product description, not a general guarantee or a drop-in integration. The general principle is to avoid exposing reusable credentials to a model or agent process.[4]

Keep the merchant authoritative

The agent's cart is a proposal. The merchant remains the source of truth for inventory, current price, taxes, discounts, fulfillment availability, and order acceptance. Revalidate the quote at the commit boundary. If the merchant returns a changed total, do not silently charge the difference. Move the order back to a state where the user can review the new terms.

This also gives the system a clean failure vocabulary. “Agent suggested a basket,” “merchant returned a quote,” “user approved,” “payment authorized,” and “merchant confirmed” are separate events. Do not display “Your order is placed” after only a tool call or payment authorization. Wait for the authoritative confirmation and show the order ID or a clear pending status.

Tests worth writing before connecting a payment provider

  • Approval for one quote cannot authorize a different quote, even if the totals match.
  • A quote past its expiration is rejected.
  • A total increase or item substitution invalidates the previous approval.
  • Two concurrent submissions produce one merchant order.
  • A retry after a lost response reuses the same idempotency key.
  • A merchant timeout produces PENDING_RECONCILIATION, not a false success or a blind second order.
  • An agent cannot alter the shipping address after approval.
  • The model's textual statement that a user approved something is never accepted as proof of approval.

These tests target the boundary between probabilistic planning and deterministic execution. They do not prove the whole checkout is safe, but they make the most consequential transitions explicit and reviewable.

A practical decision rule

Use a human-present confirmation when the order is new, high value, sensitive, or likely to involve substitutions. Consider bounded autonomous purchasing only when the user has explicitly configured a narrow policy, the merchant provides stable structured quotes, credentials are scoped, and you can reconcile uncertain outcomes. Keep the two paths separate in both code and audit records.

DoorDash's “several hundred” figure is not a verdict on agentic commerce. It is a reminder that a demo, an integration, and a habitual customer behavior are three different milestones. A useful system has to do more than call tools. It has to earn permission, preserve the exact terms that were approved, and recover safely when the network does something ordinary and inconvenient.

Checkout review checklist:

  • Is the final merchant quote immutable and expiring?
  • Does approval bind to that exact quote, user, and maximum amount?
  • Can retries create duplicate orders?
  • Who is authoritative when price or availability changes?
  • Can the audit trail explain what the user saw and what the merchant accepted?
  • Does the user have a genuinely trusted surface for confirmation?

I write about developer tools, backend systems, and AI engineering. Subscribe if you want more practical pieces like this.

Have you built an agent flow that can spend money or submit a real order? Where do you put the approval boundary?

Sources

[1] TechCrunch: DoorDash said it’s only received “several hundred” orders from AI agents
[2] DoorDash Q3 earnings-call transcript, filed with the SEC
[3] Agent Payments Protocol specification
[4] Stripe: Agentic Commerce, A Getting Started Guide

Top comments (0)