.vendo/
contract in the host repo. You, a coding agent, can complete the whole
install; vendo init does everything deterministic and prints the facts it
found.
Work from this page VERBATIM. If your fetch tool returned a summary of this
playbook instead of the raw text, re-fetch it raw
(curl -sL https://docs.vendo.run/agents.md) — the value flags, exact
commands, and rules below don’t survive paraphrase.
Only an agent in their backend? Then none of the install below applies:
that path is one package, no CLI and no init. Go straight to
In the backend.
How to read this — fetch lean, act fast. Take Start here to
the end. Init closes by naming the path this repo is on and printing ONE
Continue link; read only that path’s section below, then fetch only the page
that section names. Do NOT fetch llms.txt, llms-full.txt, or the section
for a path you are not taking — each extra fetch is a minute of latency and a
chunk of context you will not use, and it is why installs feel slow to start.
Read the repo’s package.json before deep directory scans. Start acting after
init, not after reading everything.
The code lives here, not in init’s output. Init prints no code and no
steps: it writes the files itself, then states what it wired, what it detected,
the guard posture, and one link. Every line you hand-wire is on this page and
on the quickstarts it sends you to — so when a step below and your memory of
some earlier install disagree, this page wins.
Start here
Every path shares these four steps. Do them in order, in the repo you were pointed at.-
Detect the stack. Read
package.json. Anextdependency means Next.js; anexpressdependency means Express. Anything else — Cloudflare Workers, Bun, Deno, Hono, Fastify, Lambda, bare Node — is the third scaffolded path: pass--framework customin step 3 and init writes a runtime-neutralvendo/server.tsexportinghandleVendoRequest(request, env), which your runtime mounts in one line (Edge runtimes). Workspace repos: pick the app package first. A rootpackage.jsonwith aworkspacesfield — or apnpm-workspace.yamlthat declares apackages:list — means the framework dependency lives in an app package (apps/web,packages/app, …), not at the root — and detection reads thepackage.jsonof the directory init targets. Run at the root of a workspace repo and detection finds no framework: init errors non-interactively, or lands on the runtime-neutral custom scaffold a Next or Express app should not get. Choose the app package now; the install in step 2 targets it, and every later command runs in (or is pointed at) that directory. Read that file before you believe it. Under pnpm 10pnpm-workspace.yamlis also where plain settings live —onlyBuiltDependencies,minimumReleaseAge, the exclude list in step 2 — so a copy with nopackages:key is an ordinary single-app repo, and treating it as a monorepo sends you hunting for an app package that does not exist. -
Install the package, then verify what actually resolved.
In a workspace repo, install into the app package with the host’s own package manager instead — a plain root
npm installfights the workspace lockfile:Then run the version check — and everynpx vendo …step below — from that same app directory. On pnpm only: the install downloads everything and then exits 1 withIgnored build scripts: @mongodb-js/zstd, esbuild, node-liblzma. None of the three needs compiling. pnpm writes the names intopnpm-workspace.yamlwith a placeholder — set all three tofalseand install again. Do NOT runpnpm approve-builds(it is interactive) or allow all builds.ONE package, and no others.@vendoai/vendois the umbrella: it carries every@vendoai/vendo/*subpath the files init writes import, and it puts thevendobinary intonode_modules/.bin— which is what the version check above and thepredev/prebuildhooks init adds topackage.jsonrun.vendoaiis a thin ALIAS of it, fornpx vendoai@latest …before anything is installed; as a dependency it adds nothing, and installing it INSTEAD of the umbrella is the shape that breaks — pnpm’s strictnode_modulescannot resolve@vendoai/vendo/*through an alias, so every route 500s withModule not found: Can't resolve '@vendoai/vendo/server'. The version check is not optional: stale placeholder versions (0.1.x) exist on npm, and package managers with a release-age cooldown (pnpm 11 holds new releases for a day by default viaminimumReleaseAge; npm’smin-release-ageis opt-in) will SILENTLY resolve a fresh Vendo release to that stale version — or to nothing. Everything this playbook uses (init --agent,vendo login,sync --ai) does not exist there. If the version is old or missing, exclude Vendo from the cooldown and reinstall:(npm: unsetmin-release-agefor this install, or pin the exact latest version.) Re-run the version check before continuing. Upgrading instead of installing fresh? If the repo carries a direct@vendoai/vendodependency next to thevendoaialias (common in 0.4.1-era installs), bump BOTH to the same version — bumpingvendoaialone half-upgrades: new CLI, old runtime. -
Run init in agent mode and relay its questions. Init asks; you carry
the questions to your human and their answers back. Both commands below run from
the app package directory (step 1’s pick).
vendo initcan also be pointed at it from the root (npx vendo init apps/web), butvendo loginwrites the minted key to.env.localin the directory it runs from — a login at a workspace root lands the key where the framework never loads it, and everything afterward silently behaves keyless.That run writes nothing and exits 0. It prints ONE JSON object holding the questions init still needs answered, each with its options, its recommended pick, and the flag that answers it. Put all of them to your human in your very next message, in your own words, in one turn. Keep init’s order and keep its recommendation first: “recommended” in the label is not enough if the cursor lands on the other choice. Usually there are four. How will people use your agent: embedded in their app (most apps, and the recommended pick), through their own agent loop, or from outside AI apps over MCP. Should the assistant act as their signed-in user, on the provider init detected (which preset?). Where the model comes from: a free Vendo Cloud key is one browser click and needs no provider account, or they bring their own Anthropic or OpenAI key. And where their app runs in dev — init prefills the port theirdevscript names, so this one is usually a yes, and the answer lands in.env.localasVENDO_BASE_URL. Say what each option gets their users, not what it configures. One more consent gate rides ahead of the grading pass: that pass spends model tokens, so init asks before it runs. Relay it like the rest — the answer is money, and it is not yours to give. Take the model answer first, before you re-run anything. It is the one answer that has no flag: the Vendo Cloud pick is a login you run now, and the key only counts once login has written it to.env.local. If they choose Vendo Cloud, drive the login as a bounded loop — never a single blocking call:Run this step ALONE — never batched in parallel with other commands — and the moment the URL and code print, STOP and put both in your very next message to your human: they cannot see your shell. Do not start other work until you have relayed them. Run it BARE — never pass--email, a positional email, or any identity hint. You do not know which account your human wants; they sign in as whoever they choose on the approval page, and a guessed hint (from git config or anywhere else) is an assumption, not a fact. The first run prints an approval URL and code; relay both to your human so they approve in the browser. Then re-runvendo login --wait 90in a loop until it reports the key landed in.env.local. Each re-run resumes the SAME request — no new code, no duplicate key — so the human’s approval lands whenever it arrives. Never background this command and never wrap it in a shorter timeout: a 90s bounded call returns on its own; a killed call is why you loop. The credential is delivered straight to the CLI (no email OTP, and no key ever transits your transcript; the raw protocol is auth.md). If they bring a provider key, never assume it exists — ask where it is set. Now re-run init with their answers as flags. That run writes:statustells you which run you got: the questions, or the write. Every answer on the first call skips the ask pass and writes in one go, which is right only when your human has already told you everything.--use-casetakesembedded,agent-loop, ormcp.--authtakesauthJs,clerk,supabase,auth0,jwt, ornone.--byomeans your human brings their own model key;--cloud-key <key>answers with an existing Vendo Cloud key. AVENDO_API_KEYalready sitting in.env.localneeds neither: init merges it into the run’s env, uses it as it is, and never asks. If they bring their own key, tell them to put it in.env.localasANTHROPIC_API_KEY=...orOPENAI_API_KEY=...and to tell you when it is there. It never passes through your chat.--framework <next|express|custom>matters when detection fails: agent mode does NOT stop to ask, it falls through tocustom. So a Next or Express host whosepackage.jsonyou could not read is a runtime-neutral scaffold nobody asked for — pass the flag yourself whenever step 1 left you unsure. See the CLI reference for every other flag and vendo init for exactly what it writes. -
Take the Continue link. The write run prints no code and no steps. It
ends on four computed lines and one URL — what it wired, what it detected,
the guard posture it left you, and where to go next:
The receipt carries the same as structured fields (
wrote,detected,guardPosture,continueUrl). That Continue URL is your section below, and its section names the one page to fetch:One link, one section, one page. If init named a path your human did not expect, that is a question for them, not a second install. Nothing in that tail is a paste: init writes the files itself and the code lives on the pages below. A backend agent takes none of this. That path needs no CLI and no init — see In the backend, which starts from an empty project rather than from a Continue link.
Next.js: serverExternalPackages
Every path on Next.js needs this line, so it is here rather than in four
places. Init adds it to next.config.* itself, creating the file if the repo
has none. Add it by hand when the config is one init cannot read as an object
literal (a function of phase, a computed export):
next.config.ts
@vendoai/vendo is the entry that matters and an "esbuild" entry without it
is INERT: the checker reaches esbuild through a variable specifier the bundler
cannot see, so there is no static request for Next to match. Bundled, that
import resolves from the app root where pnpm never hoists esbuild, and every
generated screen fails its checks while the app itself looks fine
(E-CFG-004). On Next 14 the key is
experimental.serverComponentsExternalPackages.
What every path shares
Read this once. Each section below says how its own path shows these to a person; the mechanics are the same everywhere. The guard’s default posture. Reads and writes RUN without approval. Destructive and ungraded calls ASK: the person gets an approve/deny card, and the call resumes on yes. A standing grant they already gave runs a destructive call without a new ask, and away runs, rate breakers, and an org policy can turn a would-be run into an ask — or into a refusal. To ask before writes too, compose the cautious preset — aguard() option, not a .vendo/policy.json
field:
.vendo/policy.json’s rules rather than tightening them. That is why
init writes guard({ policy: {} }): the empty object is the shape that keeps
reading the file.
If every call parks, the tools are ungraded, not broken. An ungraded tool
asks before each call, which reads as an agent that will not do anything.
Grade them, then restart the dev server:
VENDO_BASE_URL names your human’s own origin. Init’s dev-URL question
already wrote it into .env.local. It is load-bearing wherever a vendo_* tool
runs OUTSIDE the wire route init wrote — their own chat route — because that
call is a real HTTP request back at their API and nothing else says where to
send it. Unset there, the first tool call throws Cannot execute … set VENDO_BASE_URL, or pass baseUrl. Deployments set the same variable in the
hosting platform, to the public URL with the path prefix included
(environment variables). The MCP door reads
it for a second reason — discovery — covered in that section.
The judgment work init leaves you. Init writes the deterministic contract.
The parts that need reading the code are yours, and nothing but you will catch
a miss. Four things:
- Tool descriptions. Write each extracted tool as the task a user would ask for, not the endpoint it calls.
- Risk grades. Anything destructive or irreversible that grading missed
gets
confirmEachin.vendo/overrides.json, the file that means “a human decided this” and that sync never clobbers. - The product brief. Replace the
.vendo/brief.mdplaceholder with what this product does and for whom, read out of the code. - Theme slots. Fill anything
.vendo/theme.jsonleft unresolved from the app’s own styles.
vendo doctor --json is a
separate, optional checkup: it grades files and wiring on disk and never
contacts the running app, so a green doctor is not a working install, and a
working install does not need one. Every code it prints links to its exact fix
on the verify page.
In the product
Vendo brings the loop, the model, and the chat surface, and renders generated screens in the host’s own brand. Init wrote this path with--use-case embedded. The one page for it:
https://docs.vendo.run/product/quickstart.md.
Mount the provider, then something visible. VendoProvider is
provider-only and renders NOTHING by itself, so a repo with only the provider
has a working install nobody can see. The overlay is the visible half, and it
is a question first. Say what it is in their product’s terms: a floating
assistant button on every page, where their users ask for things like “show me
my top customers as a chart” and get a working screen built right there. Ask,
then mount both in the root layout:
app/layout.tsx
baseUrl is where init mounted the route, path prefix included — an app served
under /maple passes baseUrl="/maple/api/vendo".
Host components take TWO registrations, and they are different halves. What the
agent may build FROM is the server-side registry — createVendo({ components })
merged over .vendo/catalog.json — because that is where the descriptions and
prop schemas live. The provider’s components prop is the browser half, which
draws those names. Register in one only and you get either a component the agent
was never told about, or a name nothing can draw
(tools and catalog).
Offer slots, and explain what a slot is. A slot is a spot on one of your
human’s own pages where a user can keep a screen they built: a chart pinned to
their dashboard, still there tomorrow. Without one, everything a user builds
lives in that user’s own list of views. Say that much, then give three ways
out: name a place, let you read the app and bring back options, or skip. Say
that skipping is fine. If they want options, come back with two or three real
pages and a reason each (“the dashboard, since your data tables live there”),
and let them take one, several, or none.
label: it is what a person picking a destination reads and
what the assistant matches a request against, so write it as meaning, not as
an id. The description is how the assistant finds the right slot by meaning.
A slot with children renders them untouched while it is empty; a slot with
none shows users an invitation with prompt chips instead, which is the hint
that this space builds itself. Filled, it holds one view per person. See
where the screen lands.
Guard and done on this path. Approve/deny cards render inside the overlay,
so the person answers them where they asked. Done is their dev server running,
the overlay opening on one of their own pages, and one ask coming back as a
live screen built from their API.
In an agent you already run
The host keeps its loop, its chat, and its model; Vendo adds a guarded tool pack to it. Init wrote this path with--use-case agent-loop, which also tells
doctor not to look for a <VendoProvider> or a visible Vendo surface.
One walkthrough is your page. Fetch exactly ONE and follow it end to end —
do not read both:
- AI SDK loop →
https://docs.vendo.run/existing-agent/ai-sdk.md - Mastra loop →
https://docs.vendo.run/existing-agent/mastra.md
@vendoai/vendo/ai-sdk and @vendoai/vendo/mastra — are
the only ways the pack is delivered. A raw @anthropic-ai/sdk loop takes the
AI SDK walkthrough, and adding ai to the repo is part of that answer: say so
when you put the choice to your human, rather than after they pick.
Both walkthroughs build on one file. Init writes the createVendo call in
lib/vendo.ts (src/lib/vendo.ts when the app lives under src/), with the
wire route a thin handler over it. Init exports the caller resolver beside it —
verify both exports exist before you wire a chat route to them:
lib/vendo.ts
VENDO_BASE_URL is load-bearing on: the tool runs in
THEIR chat route, which is not a request the wire route ever sees.
Guard and done on this path. There is no overlay to place. Vendo calls
arrive in the host’s chat as dynamic-tool parts: hand every finished one to
<VendoToolResult>, which renders plain results, the live screen vendo_make
returns, and approve/deny cards, all under one provider. A chat-only host that
holds a ref it stored rather than one from a live call renders
<VendoApprovalEmbed refValue={…} /> directly instead
(embeds). Done is their own chat answering one real
ask with a Vendo card built from their API.
Outside agents over MCP
Outside agents — Claude, ChatGPT, Cursor — act in the host’s product through its own guard, riding the samecreateVendo composition every other path
writes. Init wrote this path with --use-case mcp. The one page for it:
https://docs.vendo.run/outside-agents/quickstart.md.
Init writes 1 and 2 below when the repo is Next.js on one of the four zero-arg
presets (--auth authJs|clerk|supabase|auth0). On anything else — Express,
--auth none, or --auth jwt (init prints the recipe instead: it cannot guess
a signing secret) — it writes nothing MCP, and 1 and 2 are yours:
mcp, ONE more question comes back on that run for you to
relay, and only when your human holds no Vendo Cloud key: Vendo Cloud, or their
own keys. A free Cloud key runs their models AND signs outside agents in — on
their machine while they develop, and through a stable
yourcompany.mcp.vendo.run tenant once they deploy, so none of the OAuth
surface sits on their domain and there is nothing to copy either way. Answer it
with npx vendo login (or --cloud-key <key>), or --byo for their own keys.
Init never asks WHERE outside agents sign in: it writes a VENDO_SERVICE_KEY
into .env.local, which is dev-only and gitignored, so the dev machine keeps
the door’s own OAuth and the deployment — which never sees that variable —
takes the broker. --posture broker opts out of that key for a host that wants
a Cloud-fronted door only, and pairing --service-key with it exits 1 rather
than discarding the key. Answer as flags on the next run; 3 is always yours.
The broker needs an https origin. Cloud registers VENDO_BASE_URL as the
tenant’s forwarding address and refuses one that is not https, so
--posture broker with the dev http://localhost:… above THROWS rather than
writing a door that would die on its first request. While your human is local,
that means the local posture — zero config, and it works on http. The broker is
the answer once they have an https origin to point it at.
-
mcp: trueplus the OAuth adapter. The door mints its own principals through aHostOAuthAdapter, andcreateVendoTHROWS at composition without one.authis the one door that holds it: hand it a preset, or hand it the object a preset returns and write the members yourself —oauthis a member either way. Every named preset fills it, so--auth authJs|clerk|supabase|auth0(orjwt({ secret })) is the whole answer;--auth noneis NOT — it writesauth: { principal }and nooauthmember, so the door stays shut until that member is filled. Which preset: host auth.No login yet? No preset applies — there is no session to read, and installing a provider’s SDK is not a login. Write the two members yourself, in the sameauthobject--auth nonealready wrote: keep its demoprincipal, and add anoauthwhosesessionhands the door that same subject instead of bouncing a browser — the door still renders its own consent page.LeaveactAsout. A stub returning empty headers is worse than an absent seam, because a fixed demo principal can never act as anyone else. The price of leaving it out is that the host’s own route-bound tools answernot-implementedover the door, whilevendo_makeand the apps tools work. Every client acts as that one person: a demo or an internal tool, not a multi-user product. Server-action-bound tools answernot-implementedover MCP whateveractAssays — they execute in-process with the present user’s session, and a door call has none. Top-levelprincipal,actAs, andoauthare the deprecated spelling of those same members, andmembershipshas a top-level spelling too: each still works alone, but pass any of the four besideauthandcreateVendothrows naming the mixed keys. -
The discovery route. The door’s discovery documents live at ORIGIN-ROOT
paths, outside
/api/vendo, so the catch-all route never sees them and that directory needs a handler of its own:The handler answers only the door’s own paths and 404s everything else under/.well-known. An Express host instead mounts themountVendo()that init generated into its ownvendo/server.tsat the origin root as well (app.use("/.well-known", mountVendo())) — registered AFTER the app’s own well-known routes, because that middleware is terminal: anything behind it gets Vendo’s not-found JSON. Custom runtimes route/.well-knowninto the samehandleVendoRequest(request, env)they already export. -
VENDO_BASE_URL. It names the origin the door actually answers on: the dev origin while your human is local, the deployment’s FULL public URL (path prefix included) in production. The issuer, every advertised endpoint, the protected-resource identifier, and the RFC 8707 audience all derive from it; left unset they come from the request URL — behind any proxy the proxy-internal origin, so discovery advertises endpoints no client can reach. Forwarded headers are never consulted.Init’s dev-URL question already wrote that line (.env.local--base-urlis the same answer as a flag). Production is set where the host DEPLOYS — in the platform’s own environment settings, never in a committed file — and that one stays your human’s (E-MCP-009). -
serviceAuth— only when asked for. Skip it unless the HOST’s own backend has to act for a user who is not at a browser (a nightly job, a queue worker).mcp: { serviceAuth: { keys: [...] } }opens an RFC 8693 exchange at the door’s own token endpoint: the backend posts a key plus one of the host’s user ids and gets back a short-lived token bound to that user. The keys are opaque secrets your human generates — ask before creating one. On the MCP init path a local door generates one into.env.localand wires it by DEFAULT, which is also what keeps sign-in on the dev machine; a broker-fronted door (--posture broker) serves no token endpoint of its own, so the key is the console’s to create there. Third-party agents need none of it; they run the per-user OAuth the door already serves. Recipe: MCP door.
E-MCP-004).
In the backend
Skip everything above. This path takes no CLI, noinit, and nothing moved
around — it starts from an empty project. There is no Continue link to arrive
on and no .vendo/ contract to write; if your human wants only an agent in
their backend, none of steps 1 to 4 apply. The one page for it:
https://docs.vendo.run/backend/quickstart.md.
One package and one call at module scope. agent() is the only export of the
three — the object it returns carries the other two:
chat(message)is one turn and the answer, with no route and no stream in the way.handler()is the whole agent as one fetch handler — the chat turn, the thread list and transcript, and the approvals wire — mounted on one catch-all route.run(task)is work nobody is watching.
TurnResult:
ok carries the typed output, and interrupted carries the interruptions
a person has to answer plus a resume(decisions) that carries on from exactly
where it stopped. resume() is not a rerun. A tool graded write or above is
one the guard asks about every time, so the very FIRST chat() on a fresh
agent usually comes back interrupted with nothing run — that is the guard
working, not a broken install. Done is one real turn, driven from their own
backend, answering from their own API.
Rules of engagement
- Ask your human before creating any account or key. That includes Vendo
Cloud (
vendo login), model provider keys, and sandbox accounts (E2B or the managed Cloud sandbox). Relay the choice; never sign up on your own. Look before you ask, though: aVENDO_API_KEYalready in.env.localor the environment is used as it is, so there is no key to create and nothing to ask. - Never invent props or tools outside the catalog. Only components
registered in
vendo/registry.tsxexist, and only with the props their schemas declare. Only tools in.vendo/tools.jsonexist. Copy real names from source; see tools and catalog. - Run
vendo initbefore touching.vendo/. Init writes the whole.vendo/contract (tools.json,overrides.json,policy.json,brief.md,theme.json) itself, and the AI grading pass writesjudgments.jsonbeside them. Don’t author or edit any of those files before the first init run; re-runnpx vendo initinstead of hand-writing scaffold files (the route, the registry skeleton,.vendo/contents).judgments.jsonis a LAYER, never a copy: each entry is keyed by tool name and applied on top of that tool’stools.jsonentry (description, title, risk, and the rest), withoverrides.json— the human layer — winning last. - The star ask acts only on an explicit yes. Your prompt ends by asking
your human whether to star the repo. If they say yes and the GitHub CLI is
authenticated, run
gh api --method PUT /user/starred/runvendo/vendo; otherwise print the star link from your prompt (fallback:https://vendo.run/star?src=agent) for them to click. If they say no or don’t answer, drop it — never star on their behalf without that yes.
The leaves
- Host auth: detect the host’s auth provider, wire the right preset, know when to ask the human.
- API tools: expose the host API as tools, and what the extractor reads to find them.
- Troubleshooting: every doctor error code: symptom, cause, exact fix.
Agent surfaces reference
See the CLI reference for every flag and exit code.