const BASE = "https://api.farthing.ai";
const KEY = process.env.FARTHING_KEY!;
type Json = Record<string, any>;
async function api(path: string, init: RequestInit = {}): Promise<Json> {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: {
authorization: `Bearer ${KEY}`,
"content-type": "application/json",
...(init.headers ?? {}),
},
});
// 202 (cancel) has no body; everything else returns JSON.
const text = await res.text();
const body = text ? JSON.parse(text) : {};
if (!res.ok) {
throw new Error(`${init.method ?? "GET"} ${path} → ${res.status}: ${body.message ?? text}`);
}
return body;
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
/**
* Decide what an action is asking for. `pendingUserAction.key` is set by the engine that
* raised it; the schema sniff only covers actions raised before that field existed.
* See /user-actions#identifying-an-action.
*/
function actionKey(checkout: Json): string | undefined {
if (checkout.pendingUserAction?.key) return checkout.pendingUserAction.key;
const props = checkout.pendingUserAction?.responseSchema?.properties ?? {};
return "card_number" in props ? "card" : undefined;
}
/** Pick the first allowed value of every field — fine for a demo, not for a real UI. */
function firstChoices(schema: Json): Record<string, string> {
const values: Record<string, string> = {};
for (const [name, prop] of Object.entries<any>(schema.properties ?? {})) {
const first = prop.oneOf?.[0]?.const;
if (first !== undefined) values[name] = first;
}
return values;
}
async function main() {
const profile = await api("/v1/buyer-profiles", {
method: "POST",
body: JSON.stringify({
label: "default",
name: { first: "Ada", last: "Lovelace" },
contact: { email: "ada@example.com", phone: "+15555550123" },
shipping: {
addressLines: ["1 Analytical Way"],
locality: "Louisville",
administrativeAreaCode: "KY",
postalCode: "40202",
countryCode: "US",
},
}),
});
let checkout = await api("/v1/checkouts", {
method: "POST",
// Makes this create safe to retry: the same key returns the original checkout instead of
// buying twice. Derive it from the purchase, not from the attempt.
headers: { "idempotency-key": crypto.randomUUID() },
body: JSON.stringify({
target: {
url: "https://your-sandbox-store.myshopify.com/products/widget",
request: "buy this, cheapest delivery",
},
buyerProfileId: profile.id,
constraints: { maxCost: { amount: "60.00", currency: "USD" } },
metadata: { orderRef: "acme-4412" },
}),
});
const id = checkout.id as string;
let lastSequence = 0;
// Poll. 3–5s is a reasonable interval; a full run is minutes, not seconds.
for (let i = 0; i < 300; i++) {
await sleep(4000);
checkout = await api(`/v1/checkouts/${id}`);
// Stream new timeline entries to your logs / UI.
for (const item of checkout.progressItems ?? []) {
if (item.sequence > lastSequence) {
lastSequence = item.sequence;
console.log(`[${item.sequence}] ${item.kind}: ${item.label}`);
}
}
if (["succeeded", "failed", "cancelled"].includes(checkout.status)) break;
const action = checkout.pendingUserAction;
if (!action) continue; // status may still read awaiting_user_action while it works
const key = actionKey(checkout);
const path = `/v1/checkouts/${id}/actions/${action.id}`;
if (key === "card") {
checkout = await api(path, {
method: "POST",
body: JSON.stringify({
action: "submit",
values: {
card_number: process.env.FARTHING_DEMO_CARD!,
card_expiry: "12 / 30",
card_cvc: "123",
},
}),
});
} else if (key === "variant") {
checkout = await api(path, {
method: "POST",
body: JSON.stringify({ action: "submit", values: firstChoices(action.responseSchema) }),
});
} else {
// credentials / otp / anything the tier-2 agent invented: a human has to see this.
console.log("Needs a human:", action.message, action.responseSchema);
await api(path, {
method: "POST",
body: JSON.stringify({ action: "decline", reason: "no operator available" }),
});
}
}
if (checkout.status === "succeeded") {
console.log("Order", checkout.receipt.merchantOrderId, "for", checkout.receipt.total);
} else {
console.error("Ended", checkout.status, checkout.failure?.reason);
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});