# Podium

Led product and engineering; built all agentic capabilities, KIKI World · 2022 to 2026 · Published October 6, 2026

A multi-tenant commerce API that gives many applications one end-user principal, one checkout state machine, one seller primitive, and one payment envelope that both people and agents can present.

## Thesis

Podium is a developer platform for agentic commerce, and the platform behind KIKI's community commerce application. It is a multi-tenant runtime that gives many applications four things: one end-user principal, one checkout state machine, one seller primitive, and one payment envelope that both people and agents can present.

Four units stay separate. The unit of tenancy is an organization together with a session variable that Postgres row-level security reads. The unit of a person inside that tenant is an end-user id. The unit of a purchase in flight is a checkout intent or a platform order, depending on the rail. The unit of a seller is a merchant. Collapsing those four into "the user" is the failure the contract exists to prevent.

It is not a chatbot that posts a card token, and it is not a store API with an agent bolted on. A first-party shopping-agent turn and an external agent's checkout session are different callers on the same rails. They share identity and checkout. They do not share a transcript, a spend policy, or a payment challenge.

The path to keep in mind is concrete. An application authenticates as a tenant and mints or looks up an end-user. On a confirmation from that person, the shopping agent either writes an order against a platform-owned product or creates a checkout intent against a product link. Confirming is a second call. Settlement writes a platform order id or an external order id, and a payout fan-out reads the merchant, never the agent's transcript.

## One process, ordered middleware

The API is TypeScript on Bun and Elysia: one process and one image. Postgres is session-pooled so that the tenant variable survives across a checkout. Transaction pooling would drop it between statements and make row-level security look like a bug. Redis holds cache, rate limits, a cached tenant context, and the shopping agent's chat lock. Durable asynchronous work is published to a queue from inside tenant context and consumed on webhook routes that re-enter it.

Middleware order is part of the architecture, not a detail. Cross-origin headers register first, so even error responses carry them. Raw-body parsing for signed provider webhooks comes before any JSON parser could consume the bytes. Then come timing, tenant resolution from the API key, application resolution, the public-path bypass, rate limiting, tier access, the capability gate, the payment paywall, logging, and the routes.

Reordering these is how a signature check fails with a body already read, or how a capability error leaks the existence of a route the caller's tier cannot see. The capability gate runs after the tier check for exactly that reason.

An API key is hashed before lookup, and the resolved tenant is cached briefly, with jitter. Rotating a key publishes an event, so the cache cannot outlive the rotation. Tenant id is never a caller input. A context closes over it before the handler runs, and the database client sets the session variable on every query.

## Layered identity

Identity has four layers. Mixing any two is how a checkout spoofs a cohort, or how two applications in one tenant silently share something they did not create together.

- **Tenant.** The scope of row-level security. Its settings hold the switches, allowlists, and defaults that govern commerce for that tenant. They do not hold people.
- **Application slice.** An optional header selects an application inside the tenant. A slice is a capability set and a bundle of defaults, not a second tenant. Two applications keep the same channel user apart through a synthetic, slice-qualified identity.
- **End-user principal.** One id per person per tenant. Reads and writes are filtered by tenant; uniqueness is global. So there is no portable passport across tenants: the same human in two tenants is two end-users, and one tenant can never read the other's row. Wallets are chain identity attached to the principal, not the principal itself.
- **Per-request commerce identities.** The checkout buyer, a pre-account guest, and a try-before-signup person are siblings, not children of the end-user. The checkout buyer is a contact and fulfilment record on the intent, with no user column at all.

```mermaid
%% Four identity layers. The checkout buyer hangs off the tenant, never off the end-user.
flowchart TD
  key["API key, hashed"] --> tenant["Tenant: row-level security scope"]
  tenant --> slice["Application slice: capability set"]
  tenant --> user["End-user principal"]
  user --> memory["Agent transcript and memory"]
  user --> wallet["Wallets"]
  tenant --> buyer["Checkout buyer on the intent"]
```

Checkout creation on the public REST surface is the exception that proves the split. It rejects a client-supplied user id, because a client that can pick an id can re-roll it until a rollout cohort admits them. Only a server-trusted tool path may pass an id, for cohort evaluation alone, and that id is not stored on the intent.

## One capability map

Every top-level route maps to exactly one capability, such as commerce, agentic, or identity, and an unmapped route is internal and fails closed. The map drives three generated artifacts: per-capability OpenAPI specifications, the TypeScript SDK, and per-capability MCP servers. They are consequences of one map, not three hand-kept surfaces. An application that declares commerce and agentic sees carts, checkout intents, and the shopping agent, and sees nothing else.

## The shopping agent

The shopping agent is a bounded tool loop and the first-party conversational buyer. It is not an operating system for operators, and it is not a second checkout.

- **Caps.** Tool iterations, each tool call, and the whole stream all have hard limits, with a keepalive while a long turn runs.
- **Turn lock.** A per-person lock in Redis, with a tokenized release, serializes composition for one person. It is not a durable session object.
- **Transcript.** One active conversation per person, tenant, and surface. User and model turns are written atomically, and Postgres is the source of truth.
- **Memory.** Recent history and state live briefly in Redis. A consolidation job, fired early in a relationship, periodically, and after a gap, distils the conversation into a durable intent profile, which is living memory for the next turn.
- **Metering.** Each authenticated turn records usage under a per-request deduplication key, so a retry is not a second charge.
- **Spend policy.** Daily and per-transaction caps run before placement, summed over the person's live orders.

The agent's ability to buy from an arbitrary link is a schema decision, not a prompt decision. A flag decides whether the link field exists in the tool schema at all, and it fails closed to off. It is on only when the tenant has enabled that rail and the person is in an active cohort. When it is off, the model cannot emit a link, because the field is absent. Prompt copy and tool schema are built from the same flag, so they cannot disagree.

The agent mints but does not settle. On a platform product, it writes an order awaiting payment. On a link, it creates a checkout intent and stops, and the client mounts the confirmation surface. A person, an external session's completion, or an agent's payment closes the loop.

## Universal checkout

Universal checkout is a two-phase state machine that puts platform-owned products, connected Shopify stores, and an external commerce provider behind one handle.

```mermaid
%% Two phases. Any non-terminal state may fail; a terminal state has no way out.
stateDiagram-v2
  processing: Processing
  awaitingConfirmation: Awaiting confirmation
  confirming: Confirming
  completed: Completed
  failed: Failed
  [*] --> processing
  processing --> awaitingConfirmation: offer resolved, adapter locked
  processing --> failed: offer cannot resolve
  awaitingConfirmation --> confirming: buyer confirms
  awaitingConfirmation --> failed: fails before confirming
  confirming --> completed: order placed
  confirming --> failed: placement fails
```

An illegal transition is a conflict error, and a second terminal is refused.

**Create.** A product lookup, then adapter selection, then the adapter resolves an offer. If no adapter can serve the link, creation fails with a clear unsupported-merchant error and leaves no stub row.

**Routing is ordered and not commutative.** A platform-owned product goes to the native adapter. A host on a connected Shopify domain goes to the Shopify adapter. A host the external provider covers, for a person in the cohort, goes to that provider. Anything else is unsupported. Reordering those rules would change who fulfils an order.

**Confirm.** The adapter is locked when the intent is created, and confirmation uses that adapter by name. A kill switch flipped mid-flight stops new intents. It does not rewind one already in flight onto a different rail. Price and shipping ceilings the caller set are honoured when the offer is made, and confirmation does not re-litigate them against a new quote.

**Recover.** Provider webhooks are signed, and thin ones are treated as a prompt to re-fetch canonical state rather than as the state itself. A periodic reconciler sweeps intents stuck in confirmation and resolves the uncertain outcome. Telemetry records one row per terminal intent.

## Payment envelopes

Payment arrives through two protocols, kept as separate patterns.

**x402.** The API speaks version 1 of x402, the HTTP 402 payment protocol, in two patterns that share one verify and settle service. As a paywall, a configured route without a valid payment returns a protocol-correct 402 listing its requirements. Verification runs before the handler and settlement after it succeeds. As order payment, an agent pays a specific order, to an address the tenant configured. One payment row per order makes a double payment a constraint violation, and a verified-but-unsettled payment can be replaced only after it goes stale.

**ACP.** The Agentic Commerce Protocol is an open standard for checkout sessions. The API implements an ACP-shaped rail for external agents: session create, update, and complete, with buyer constraints in the body and a product feed for agents. Maximum total and maximum shipping are enforced against live state before any charge.

An ACP session id, a checkout intent id, a shopping-agent order id, and an external order id are four different handles that may all point at one platform order. No single sentence about "agent checkout" covers all of them, and the code does not pretend one does.

## Multi-seller settlement

A merchant is the canonical seller. A product with no merchant is platform-owned: it participates in checkout and buyer email, and generates no payout and no seller notification.

Shipping status lives on each order item, and the order's status is a rollup in which the least progressed item wins. Application code never writes order-level shipping status directly. Quotes and labels are scoped to one merchant each.

Settlement extends Stripe Connect. A destination charge cannot split funds, so the platform takes the payment itself whenever an order has more than one merchant, any platform-owned item, or a merchant without a connected account. A single connected merchant takes the direct path.

```mermaid
%% Retries and webhook replays cannot pay a seller twice.
flowchart TD
  paid["Payment succeeded"] --> group["Group items by merchant"]
  group --> alloc["Deterministic allocation: discount, fee, shipping"]
  alloc --> payout["One payout record per order and merchant"]
  payout --> transfer["Idempotent transfer keyed by the payout"]
  payout --> notify["One seller notification per merchant"]
```

On payment success, items group by merchant. Discounts, fees, and shipping allocate deterministically, rounding down with the remainder to the first share, and quoted shipping wins when it reconciles. There is exactly one payout record per order and merchant, guarded by a database uniqueness constraint. The transfer's idempotency key derives from that record, so a collision is a replay and never a second transfer. A seller may update only its own items.

## The event fabric

Events publish from inside tenant context, and a publish from outside it is a silent drop, so publishing outside it is treated as a defect. Every handler must be safe to replay. Idempotency is local to each handler, either through a deduplication id derived from the order and merchant or through a database uniqueness constraint that makes the second write fail.

## What it buys

- A new application on a tenant is an application id plus a capability set, not a new user table.
- A new seller is a merchant row, and multi-seller carts still settle exactly once per seller.
- An agent can start a checkout without being able to settle it.
- Card and x402 payments close orders the same way.
- No client can spoof its way into a rollout cohort through a request body.
