Guide: build a shopping agent (SDK)
Goal: you are the platform. You run your own assistant or app that searches,
checks out, and completes purchases on behalf of your signed-in buyers — on
card or crypto — using the @artos-commerce/ucp-client
SDK rather than the hosted bridge.
Who this is for: product teams embedding agentic commerce. If you just want Claude/Cursor to shop, use Connect an assistant — zero code. If you only read the catalog, use Catalog crawler.
What you own vs what the SDK owns
| You own | The SDK owns |
|---|---|
| A platform API key (get one) | The UCP transport + envelope, idempotency |
A hosted agent profile at /.well-known/ucp | The two-credential auth model + 401 relay |
| An AP2 signing key (public JWK published in the profile) | AP2 mandate minting + byte-parity canonical JSON |
| Buyer OAuth 2.1 + PKCE (acquire + refresh the bearer) | Merchant-terms verification, rail selection |
| A CIMD document (only because you run buyer OAuth) | Card / crypto / $0 settlement orchestration |
Phased checklist
Phase 0 — credentials
- Request a platform API key (
ck_<id>.<secret>). - Generate an AP2 signing key + profile:
artos profile init --url https://your-app.example/.well-known/ucp(see CLI). Hostucp.jsonpublicly; keepprivate-jwk.jsonasAGENT_PRIVATE_JWK. - Host a CIMD
oauth-client.json(sample below) so buyers can consent.
Phase 1 — verify — artos doctor (your setup) and artos conformance
(the deployment’s UCP contract); gate the latter in CI (Conformance & CI).
Phase 2 — flows — search → checkout → confirmPurchase; then buyer OAuth +
the account tools; then the crypto rail.
Phase 3 — production — point at https://api.artos.sh, store rotating refresh
tokens, relay 401 for silent refresh, never ship the buyer bearer to a browser.
Environment
# .env (server-side only — never expose the buyer bearer to a browser)
ARTOS_BASE_URL=https://api.artos.sh
UCP_PLATFORM_API_KEY=ck_xxx.secret_xxx # platform key (cross-store)
PLATFORM_PROFILE_URL=https://your-app.example/.well-known/ucp
AGENT_PRIVATE_JWK={"kty":"EC","crv":"P-256",...} # AP2 signing key (secret)
AGENT_KID=your-key-id # matches the public JWK in your profile
# Crypto rail only:
AGENT_SUI_PRIVATE_KEY=suiprivkey1...
# Buyer OAuth (your CIMD client):
OAUTH_CLIENT_ID=https://your-app.example/oauth-client.json
OAUTH_REDIRECT_URI=https://your-app.example/callbackBootstrap + buy
Crypto symbols live off the root barrel so card-only builds never load
@mysten/sui. Import them from the /crypto and /sui subpaths.
import {
UcpClient,
Ap2Signer,
createCheckoutHandlers,
} from '@artos-commerce/ucp-client';
import { resolveCryptoDeps } from '@artos-commerce/ucp-client/crypto'; // crypto subpath
const client = new UcpClient(
{
artosBaseUrl: process.env.ARTOS_BASE_URL!,
platformApiKey: process.env.UCP_PLATFORM_API_KEY!,
platformProfileUrl: process.env.PLATFORM_PROFILE_URL!,
},
{
buyerToken: session.buyerBearer, // server-side; omit for card-only/anonymous
onAuthChallenge: (wwwAuthenticate) => respondWith401(wwwAuthenticate),
},
);
const signer = new Ap2Signer(
JSON.parse(process.env.AGENT_PRIVATE_JWK!),
process.env.AGENT_KID!,
);
// Optional crypto rail — returns undefined when no Sui key is set.
const crypto = resolveCryptoDeps({
agentSuiPrivateKey: process.env.AGENT_SUI_PRIVATE_KEY,
suiNetwork: 'mainnet',
agentMaxSpendAmount: 50_000, // optional client-side cap (minor units)
});
const shop = createCheckoutHandlers({ client, signer, crypto });
const results = await shop.searchProducts({
query: 'running shoes',
filters: { price: { max: 150, currency: 'USD' } }, // MAJOR units
});
const checkout = await shop.createCheckout({
storeSlug: 'YOUR_STORE_SLUG', // from each result's metadata.artos_seller.slug
items: [{ id: 'variant_123', quantity: 1 }],
shippingAddress: { country: 'US', postal_code: '94016' },
});
const outcome = await shop.confirmPurchase({
storeSlug: 'YOUR_STORE_SLUG',
checkoutId: checkout.id as string,
paymentMethod: 'artos.card', // omit on a multi-rail store to be asked
});Handle the outcome
confirmPurchase never throws for a business outcome — it resolves to a
CheckoutOutcome you switch on:
switch (outcome.status) {
case 'completed':
return renderOrder(outcome.checkout);
case 'escalation_required':
return redirect(outcome.continueUrl); // 3-DS / hosted payment step
case 'payment_selection_required':
return askBuyerToPickRail(outcome.rails); // retry with paymentMethod
case 'error':
// code: crypto_not_configured | authentication_required |
// merchant_authorization_invalid | spend_cap_exceeded | payment_execution_failed | …
return showError(outcome.code, outcome.message);
}Transport failures (network / non-2xx / JSON-RPC) still throw UcpClientError;
an expired buyer bearer throws UcpAuthError carrying the WWW-Authenticate
challenge (relayed via onAuthChallenge for silent refresh).
Buyer OAuth (you own this)
The SDK forwards and relays the buyer bearer but does not acquire it. Use
OAuthClient + createPkce:
import { OAuthClient, createPkce, DEFAULT_BUYER_SCOPE } from '@artos-commerce/ucp-client/oauth';
const oauth = new OAuthClient({ artosBaseUrl: process.env.ARTOS_BASE_URL! });
await oauth.discover();
// 1. Start consent.
const pkce = createPkce();
const url = oauth.authorizeUrl({
clientId: process.env.OAUTH_CLIENT_ID!, // your CIMD URL
redirectUri: process.env.OAUTH_REDIRECT_URI!,
codeChallenge: pkce.challenge,
scope: DEFAULT_BUYER_SCOPE, // "purchase:complete offline_access"
});
// redirect the buyer to `url`; stash pkce.verifier in the session.
// 2. On the callback, exchange the code.
const tokens = await oauth.exchangeCode({
code, codeVerifier: pkce.verifier,
clientId: process.env.OAUTH_CLIENT_ID!,
redirectUri: process.env.OAUTH_REDIRECT_URI!,
});
// store tokens.refresh_token (rotated, single-use) and use tokens.access_token as buyerToken.Minimal CIMD document
Host this at the URL you use as client_id. List every redirect URI you use
(a callback not in the document is rejected). See
Authentication → CIMD.
{
"client_id": "https://your-app.example/oauth-client.json",
"client_name": "Your App",
"client_uri": "https://your-app.example",
"redirect_uris": ["https://your-app.example/callback"],
"token_endpoint_auth_method": "none"
}The crypto rail
To settle on-chain, pass resolveCryptoDeps(...) into createCheckoutHandlers
and confirm with paymentMethod: 'artos.crypto'. The SDK prepares the payment
intent (buyer bearer only), re-prices, re-verifies the merchant authorization,
mints the checkout_mandate and payment_mandate, signs/submits the Sui PTB
via SuiSigner, then completes with the tx digest.
Crypto is buyer-bound and has two hard requirements beyond OAuth consent:
@mysten/suiinstalled andAGENT_SUI_PRIVATE_KEYset (elseoutcome.code === 'crypto_not_configured').- The buyer authorized your agent under My Artos → Agents with a spend cap
(else
authentication_required/spend_cap_exceeded). See Authentication → spend caps.
Buyer-account tools
With a buyer bearer set, reach the account surface through createAccountHandlers
(buyer bearer only — never the platform key):
import { createAccountHandlers } from '@artos-commerce/ucp-client/account';
const account = createAccountHandlers({ client });
const ctx = await account.getBuyerContext(); // default address, spend caps
const orders = await account.listOrders(); // cross-store history
const order = await account.getOrder('order_789'); // sends { order_id }See Buyer account for the full tool set.
Next steps
- Build with the SDK — the full API surface
- Authentication — OAuth, CIMD, scopes, spend caps
- Checkout — rails, AP2, coupons,
$0orders - Conformance & CI — gate the deployment contract
- Checkout handoff — prepare a cart, hand off a payment link