Blog · Architecture

Agentic Commerce Without Write Access: The Task Handoff Pattern

A safer pattern for AI commerce: hand the task back to the human and the merchant, then verify the result.

AI Agents Commerce Security Architecture Customer Experience
2Ryun team Read-only by design
Task handoff pattern cover illustration

The demo looks incredible.

A customer types “upgrade me to the annual Pro plan.” The agent understands, picks the plan, applies the discount, calls your billing API, and replies:

“Done! You’re on the Pro plan. Your card was charged $399.”

Everyone in the room applauds. Then you ask the obvious question: what happens if the model hallucinated the plan, the discount, or the customer?

That is where most “agentic commerce” demos quietly end.

At 2Ryun, we took a different position: the agent should not perform the transaction. It should hand the transaction back to the system that is allowed to own it.

We call this pattern Task Handoff, and it is the mechanism behind purchases, bookings, verification flows and other business actions in our support bot.

Why write access is a trap

Giving an LLM a write tool looks like a feature. Operationally, it is a bundle of hard problems:

  • Idempotency. A network retry or an impatient user can create two orders.
  • Authorization. Owning an identity is not the same as owning permission to spend money.
  • Irreversibility. A wrong refund, cancellation, or contract signature is not a prompt you can re-run.
  • Prompt injection. A malicious paragraph in a knowledge document can try to trigger an action.
  • Payment boundaries. Card data, 3-D Secure, invoicing, and tax live in the merchant’s stack — usually for good regulatory reasons.
  • Audit. “The model said so” is not a transaction log.

The deeper issue is philosophical: a support agent’s job is to help a person complete a task, not to become the merchant.

The handoff pattern

Instead of a write tool, the agent gets a handoff:

  1. Show the user the relevant option (plan, booking slot, verification flow).
  2. Open the merchant’s own page — or call a function the merchant registered on their own page.
  3. Let the merchant handle login, payment, inventory, terms, and confirmation.
  4. Verify the outcome through a read-only status endpoint.
  5. Report the result back in the conversation, with a receipt-style card.

The platform never gets a write credential. The merchant never gives up control of checkout. The user gets an agent that can help without becoming a payment processor.

Two entry types, one contract

A handoff can open a page:

https://shop.example.com/buy?plan=pro&ref=01M6912WXFSPB9GNTNR9SD95T9

…or call a function that already exists in the merchant’s page:

window.__ryunBotActions = {
  async checkout(args, ctx) {
    // args: { plan: "pro" }
    // ctx:  { taskId, taskRef, visitorId, sub, lang, params }

    const res = await fetch("/api/cart/checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ ref: ctx.taskRef, plan: args.plan }),
    });
    const data = await res.json();

    if (data.requiresPayment) {
      openCheckout(data.checkoutUrl);
      return { status: "pending" }; // final result comes from the status API
    }
    return {
      status: data.ok ? "completed" : "failed",
      result: { order_id: data.orderId, amount: data.amount },
    };
  },
};

The page function is merchant-owned code running in the merchant’s origin. It can use the merchant’s cookies and session. The platform only invokes it, validates the origin, and treats the return value as untrusted input.

If the flow needs more time — a payment, a KYC review, a signature — the function returns pending.

Diagram of write-access risk versus read-only handoff

The correlation problem

If the user leaves the chat and buys on the merchant page, how does the bot know which order to check?

Every handoff creates a task_ref — an opaque, time-ordered identifier. It is appended to the entry URL, or passed to the page function. The merchant stores it next to the order or booking.

The status check is then a simple read:

GET https://shop.example.com/api/purchase/status?ref=01M6912WXFSPB9GNTNR9SD95T9
{
  "data": {
    "status": "paid",
    "order": { "id": "20261031-001", "amount": "399" },
    "updated_at": "2026-10-31T09:00:03Z"
  }
}

No user ID in the URL. No shared secret in the browser. No write endpoint exposed to the agent.

If the merchant cannot store a ref, a fallback is sub + business key — but the preferred design is an opaque reference.

Who is the user?

Personal results need identity. We deliberately do not share sessions or cookies with the merchant’s backend.

Instead, the merchant backend — which already knows the user from its own login session — signs a short-lived JWT:

const token = jwt.sign(
  {
    sub: String(user.id),
    bot: process.env.BOT_ID,
    exp: Math.floor(Date.now() / 1000) + 600,
  },
  process.env.BOT_SIGNING_SECRET,
  { algorithm: "HS256" },
);

The token is passed through the widget’s data-user-token attribute. The platform verifies the signature and extracts sub.

One clarification that saves hours of debugging: the secret is not the token. BOT_SIGNING_SECRET is a shared HS256 key that lives only on the server. The token is a short-lived assertion signed with that key, one per user, per page load. The platform also issues two internal, platform-signed tokens — an offer for card parameters and a task_token for status checks — but neither is a user identity, and your backend never needs to handle them.

Crucially:

  • the model never sees or chooses the user ID;
  • a visitor parameter is always overwritten by the verified sub;
  • a token signed for another bot is rejected;
  • no token means no personal data — not “best effort.”

This is the same conceptual move as OAuth: prove the user without sharing the password. The difference is transport. OAuth delegates authorization through a browser consent flow; a signed JWT delegates identity from a backend that already trusts the user. For an embedded support widget, the signed assertion is dramatically simpler to adopt.

The state machine

A handoff is a small, boring state machine — which is exactly why it works:

created → awaiting_user → verifying → completed
                      ↘            ↘ failed
                       ↘ cancelled  ↘ expired
  • created / awaiting_user: the entry has been handed to the user.
  • verifying: the platform is polling the read-only status endpoint.
  • completed / failed / cancelled / expired: terminal states.

Defaults matter:

  • polling backs off (5s → 15s → 30s → 60s, with jitter);
  • the total timeout is 15 minutes by default;
  • an expired task keeps its entry link so the user can try again;
  • if the user closes the chat, server-side polling continues;
  • when they return, active tasks are restored from the conversation.

The UI is equally unglamorous: a waiting card, then a result card with an order ID, amount, and timestamp. The result fields come from the merchant’s status API — not from the model.

What the user sees

You:      Upgrade me to Pro.
Bot:      Pro is $399/year. Here are the details.
          [ Buy Pro ]  [ Compare plans ]

          ┌───────────────────────────────┐
          │  Pro upgrade · Waiting         │
          │  Complete checkout on the      │
          │  merchant page.                │
          │  [ Cancel ]                    │
          └───────────────────────────────┘

          (user pays on the merchant page)

          ┌───────────────────────────────┐
          │  Pro upgrade · Completed       │
          │  Order   20261031-001          │
          │  Amount  $399                  │
          └───────────────────────────────┘

No confetti. No “the AI just bought something.” Just a clear, auditable handoff.

Why not just use a function-calling tool?

You can absolutely expose create_order() as a tool. Many teams do. But then you own:

  • the idempotency key strategy;
  • the authorization check behind create_order();
  • the user-confirmation UX;
  • the failure and retry semantics;
  • the audit trail that proves a human intended the action.

A handoff pushes those responsibilities to the system that already has them: the merchant’s checkout. The agent keeps the part it is actually good at — understanding intent, comparing options, and explaining what happens next.

For low-risk, internal, or fully reversible actions, a write tool can be reasonable. For money, contracts, and account changes, the handoff is the safer default.

User journey of a task handoff from chat to merchant checkout

The boundary keeps everyone honest

The platform is read-only.

It cannot charge, cancel, refund, or sign. That is a feature you can put in a security review.

The merchant owns the transaction.

Its existing login, payment, inventory, and compliance controls remain in force.

The user stays in control.

They see where they are going, complete the action in a familiar interface, and get a result back in the conversation.

The model stays in its lane.

It can suggest and explain; it cannot execute.

Roadmap, honestly

Our first version supports polling and immediate page-function results. Webhook callbacks — signed, deduplicated, and preferred over polling — are next. Redirect-back after checkout is a UX enhancement, never a source of truth.

We are also exploring exposing task status as an MCP tool, so other agents can query the same handoff records. The protocol changes; the boundary does not.

The lesson

Agentic commerce does not have to mean “the agent has your credit card.”

It can mean: the agent finds the right option, opens the right door, and verifies that you walked through it. The transaction still belongs to the merchant. The intent still belongs to the user. The agent’s job is the space between them.

Don’t let your agent click “buy.” Let it hand you the checkout page — and then check the receipt.

Building this on 2Ryun

2Ryun ships this boundary as a product: task handoff to a merchant URL or a page-level function, short-lived signed visitor identity, read-only status verification, and no write credentials for the platform — by design.

Start free on 2Ryun

This is the second story in a two-part series. Read the first: Your AI Support Bot Is Confidently Wrong About Prices.