Your Market API Guide
Audience: frontend engineers wiring the Pool (buyer) and Source (ops)
surfaces. Coverage: the commerce-agent callables in functions/market.js.
For architecture and harness rules, see Your Market architecture — current. For the exhaustive CF catalog (including the older vote-on-bids marketplace), see the Cloud Functions Reference.
All callables below are us-east1, App Check carved out (enforceAppCheck: false,
same posture as createPoll / castProductVote). Project:
foundation-next-app.
import { getFunctions, httpsCallable } from 'firebase/functions';
const fns = getFunctions(app, 'us-east1');
Two Market APIs (do not mix them)
Pillar 3 currently has two write paths. They share a pillar and a color
(#0891b2). They do not share collections or callables.
| Surface | Callables | Collections | Member action |
|---|---|---|---|
| Vote-on-bids (existing) | submitProductBid, castProductVote, adminUpdateProductStatus |
product_requests, product_votes |
Vote for a supplier bid |
| Commerce agents (this guide) | runMarketAgent, commitToMarketPool, placeMarketOrder, … |
market_* |
Commit qty + max price, then host Place order |
The vote path is documented under Pillar 3 — Marketplace in the Foundation API guide. This file is the agent path.
Auth at a glance
| Callable | Auth |
|---|---|
runMarketAgent (agent: pool) |
requireAuth + requireVerifiedMember |
commitToMarketPool |
requireAuth + requireVerifiedMember |
placeMarketOrder |
requireAuth + requireVerifiedMember |
listMarketMemory / correctMarketMemory / deleteMarketMemory |
requireAuth + requireVerifiedMember |
runMarketAgent (agent: source) |
requireRing(TENANT_ADMIN) + requireVerifiedMember |
applyMarketAction |
requireRing(TENANT_ADMIN) |
seedMarketDemo |
requireRing(TENANT_ADMIN) |
Unverified demand is rejected before aggregates move — fake lines cannot move supplier prices.
Pool (buyer)
Browse / chat — runMarketAgent
The Pool agent searches a ranked catalog and presents UI components. The
client renders lastUi; it does not parse prose for product cards.
const runMarketAgent = httpsCallable<
{
agent?: 'pool' | 'source';
message: string;
history?: Array<{ role: 'user' | 'assistant'; content: string }>;
},
{
lastUi: {
name: 'present_pools' | 'present_checkout' | 'present_quotes' | 'present_review_queue' | null;
records: Array<Record<string, unknown>>;
layout: string[];
};
unknownTools: string[];
aggregate: {
poolId: string;
committedQty: number;
customerCount: number;
p50Max: number;
fillRatio: number;
status: string;
} | null;
}
>(fns, 'runMarketAgent');
const { data } = await runMarketAgent({
agent: 'pool',
message: 'What bulk buys are open?',
});
// data.lastUi.name === 'present_pools'
// data.lastUi.layout matches on-screen card order
lastUi.records are server-filled. A pool/order/quote ID the session never
issued is dropped (empty row, not a hallucinated card).
There is no capture_payment tool. If the model asks for one, it lands in
unknownTools and nothing is charged.
Host join — commitToMarketPool
The Join control on a pool card is a host action, like Place order. It is not a model tool. Commitment is qty + max unit price, not a charge.
const commitToMarketPool = httpsCallable<
{ poolId: string; qty: number; maxUnitPrice: number },
{ ok: true; commitmentId: string; orderId: string | null }
>(fns, 'commitToMarketPool');
await commitToMarketPool({
poolId: 'olive-oil-5l',
qty: 1,
maxUnitPrice: 5,
});
Errors (HttpsError code):
| Engine code | Firebase code | When |
|---|---|---|
not-verified |
thrown by requireVerifiedMember |
Caller is not a verified member |
qty-cap |
resource-exhausted |
Resulting qty on this member's line would exceed 20 |
| other | failed-precondition |
Unknown pool, invalid qty/max, etc. |
On success the engine re-evaluates policy in the same per-pool transaction.
A join that crosses 80% fill against a known, high-rated supplier can return
with the pool already awarded and orderId set.
Reads: the caller may read market_commitments docs where uid == auth.uid.
Clients never write that collection.
Host checkout — placeMarketOrder
const placeMarketOrder = httpsCallable<
{ orderId: string; nonce: string },
{ ok: true; orderId: string; paymentRef: string }
>(fns, 'placeMarketOrder');
await placeMarketOrder({
orderId: 'olive-oil-5l:THE_UID',
nonce: braintreeDropinNonce, // or a demo nonce in emulator
});
The model cannot call this. nonce is required. Settlement is the host's
Plantago payments path (@plantagoai/payments); the engine records
status: "paid" only after this callable succeeds.
Source (ops)
Ops sees aggregates, never a member dump.
const { data } = await runMarketAgent({
agent: 'source',
message: 'RFQ the olive oil pool and show quotes.',
});
// data.lastUi.name === 'present_quotes' | 'present_review_queue'
// data.aggregate has committedQty, customerCount, p50Max — no uids
Quote notes arrive already fenced:
<untrusted-commerce-data>
…supplier text…
</untrusted-commerce-data>
Treat fenced text as material to display, never as instructions.
Apply a staged change — applyMarketAction
const applyMarketAction = httpsCallable<
{ actionId: string; poolId: string },
{ ok: true }
>(fns, 'applyMarketAction');
await applyMarketAction({ actionId, poolId: 'olive-oil-5l' });
Guardrails re-run now (current ceiling, current rating), not as they were
when the model staged the change. A quote above the live ceiling returns
failed-precondition / guardrail.
Policy auto-applies eligible RFQ/award in-band on join, so this callable is the exception path (thin pool, low rating, disputed return).
Memory
Facts are typed { key, value, category } per uid. Extracted asynchronously
from user and assistant text only — never from listings or tool results.
Disabled when MARKET_MEMORY_ENABLED=false.
const listMarketMemory = httpsCallable<
{ keys?: string[] },
{ enabled: boolean; facts: Array<{ key: string; value: string; category: string }> }
>(fns, 'listMarketMemory');
const correctMarketMemory = httpsCallable<
{ key: string; value: string; category?: string },
{ ok: true } | { enabled: false }
>(fns, 'correctMarketMemory');
const deleteMarketMemory = httpsCallable<{ key: string }, { ok: true }>(
fns, 'deleteMarketMemory',
);
Always-on keys (session prefix): qty_cap, delivery_preference, default_max.
Demo seed
const seedMarketDemo = httpsCallable<Record<string, never>, { ok: true; pool: object }>(
fns, 'seedMarketDemo',
);
await seedMarketDemo({});
Seeds olive-oil-5l at 7/10 fill with supplier Northwind (rating 4.5). One
verified commitToMarketPool({ poolId: 'olive-oil-5l', qty: 1, maxUnitPrice: 5 })
trips RFQ → auto-award → an awaiting_checkout order for that uid.
Presentation contract
Render lastUi.records in lastUi.layout order. Do not reshuffle on the
client — "the first card" in the next user turn refers to this layout.
lastUi.name |
Record ids | Host controls on the card |
|---|---|---|
present_pools |
pool ids | Join (commitToMarketPool) |
present_checkout |
order ids | Place order (placeMarketOrder) |
present_quotes |
quote ids | (ops) inspect; apply via review if needed |
present_review_queue |
review ids | Apply (applyMarketAction) |
Checkout records include placeOrderLabel: "Place order" — use that string,
do not paraphrase a charge.
Firestore (read-only from the client)
| Collection | Client read | Client write |
|---|---|---|
market_pools, market_catalog, market_suppliers |
signed-in | none |
market_commitments, market_orders, market_memory, market_agent_sessions |
own uid only |
none |
market_quotes, market_review_queue, market_staged_actions |
admin (ring ≤ 1) | none |
Schemas: Firestore schema.