A buyer that is software
services.dev/agent buys from any brand by its card. It asks the price, orders within a budget, waits through any person's line, and accepts.
services.dev/agent is a buyer in a few lines of code. It needs no generated client. It reads a brand's /.well-known/agents.json, connects to the MCP server the card names, and calls the same tools any agent would.
import { connect } from 'services.dev/agent'
import translation from './services/translation.ts'
const lingua = connect('http://localhost:8787', { budget: 100 })
const lease = { url: 'https://example.com/lease.pdf', name: 'lease.pdf' }
const job = await lingua.order(translation, { document: lease, to: 'fr-CA' })
if (!('done' in job)) throw new Error(job.type === 'OFFER' ? job.price.sentence : job.reason)
const done = await job.done()
if (done.status !== 'delivered') throw new Error(`${done.id} ${done.status}`)
console.log(`${done.id} · delivered · ${done.output.document.name}`)
await job.accept()Run it beside npx services.dev:
$ node buy.ts
job_7Hq2 · delivered · lease.fr-CA.pdf
job_7Hq2 · accepted · spent $49 of $100order() sends the input without max_usd, so the first answer is the OFFER. When the price fits what's left of the budget, it sends the offer's next back unchanged, key included, so a retry never orders twice. When it doesn't fit, order() returns the offer for you to show a person.
→ GET /.well-known/agents.json
← 200 · lingua · mcp http://localhost:8787/mcp
→ initialize
← lingua · "Without max_usd you get an OFFER: send its next call unchanged to order."
→ tools/list
← order_translation "Translation: send a document and a language; get a document back. $49 per job, within 48 hours, charged on delivery. AI delivers every job."
get_job · accept_job · send_back_job · cancel_job · and 8 more
→ order_translation {document: document.pdf, to: "fr-CA", key: "lease-fr-CA"}
← OFFER · $49 per job, within 48 hours, charged on delivery.
next: order_translation {document: document.pdf, to: "fr-CA", max_usd: 49, key: "lease-fr-CA"}
→ order_translation {document: document.pdf, to: "fr-CA", max_usd: 49, key: "lease-fr-CA"}
← job_7Hq2 · working · $49.00 held
→ get_job {id: "job_7Hq2", wait: 50}
← job_7Hq2 · delivered · document.fr-CA.pdf · $49.00 charged
stage: review · payment: charged $49
→ accept_job {id: "job_7Hq2"}
← job_7Hq2 · accepted · document.fr-CA.pdf · $49.00 finalexport const METHODS = {
order: 'service.order',
get: 'job.get',
done: 'job.wait',
accept: 'job.accept',
sendBack: 'job.send_back',
cancel: 'job.cancel',
} as const satisfies Record<Exclude<keyof Agent, 'spent'> | Exclude<keyof Bought, 'id' | 'job'>, OpId>Each function calls its row's MCP tool, so the agent can do exactly what the tools allow. It can't do what only a person does (What an agent can never do).
done() long-polls until the job is delivered, accepted or canceled. A job waiting on a party comes back at once, with the link only that person can use:
const job = await agent.order('engagement', { client: ada, matter: 'a lease review' })
← job_7Hq2 · working · $120.00 held
const now = await job.done()
← job_7Hq2 · waiting · on party · $120.00 held
next {"do":"hand_to_person","url":"http://localhost:8787/a/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_","to":"sign"}
agent.spent
← 120Hand the link on. Once they act, done() waits through the rest. Over the buyer key's cap, the offer's next is checkout, a person's step, so the agent returns the offer without spending.
Worked in: the agent's order, one deal from the OFFER to the payment.
Your brand's typed client throws a refusal. The agent returns it: order() answers a bought job, an offer or a refusal, so a loop can tell the three apart and decide what to do next.
export interface AgentOptions {
/** The most this agent spends in all, in dollars. */
budget?: number
/** The buyer key, sent as Authorization: Bearer. On localhost dev takes a call with none. */
key?: string
/** How requests go out: a recorder, or a server in the same process. */
fetch?: (request: Request) => Promise<Response>
}budget caps what the agent spends across every order, and spent reads it back.