Protocol 02
Spec v1Your API is probably already compatible.
Publish two static JSON files describing what it can do. Every Interagentic client can then discover and call it — with help, types, and examples — and you never write an SDK.
$npx interagentic popcorn.clubPopcorn Club · 6 actions · auth: interagentic orders Place and track orders subscriptions Recurring deliveries $npx interagentic popcorn.club orders list List your orders create Place an order [$] ship Mark an order as shippedAdoption cost
Nothing about your API has to change.
No new runtime, no gateway in front, no rewrite. You keep your routes, your handlers, your validation and your error shapes. You add a description of them, and one auth check.
// app/api/v1/orders/route.ts
export async function POST(req: Request) {
const { qty } = await req.json();
const order = await createOrder(qty);
return Response.json(order);
} // app/api/v1/orders/route.ts
+ import { auth } from "@interagentic/nextjs/server";
export async function POST(req: Request) {
+ const { subject } = await auth(req).protect();
const { qty } = await req.json();
- const order = await createOrder(qty);
+ const order = await createOrder(qty, subject);
return Response.json(order);
}That is the whole integration
Two lines in the handler, plus two static files under/.well-known/ that you can generate from an OpenAPI document. If you already publish OpenAPI, this is a build step, not a project.Command model
A command is a list of words.
An action declares its command as an array of segments. The CLI turns that into the subcommand chain every developer already knows from git, gh and vercel — at any depth, with no special cases.
{
"name": "Popcorn Club",
"description": "Popcorn, delivered.",
"version": "1",
"auth": "interagentic",
"baseUrl": "/api/v1",
// Groups let an intermediate node describe itself, so a partial
// chain can print useful help instead of an error.
"groups": [
{ "command": ["orders"], "description": "Place and track orders" },
{ "command": ["orders", "refunds"], "description": "Issue refunds" }
],
"actions": [
{
"command": ["orders", "create"],
"endpoint": "/orders",
"method": "POST",
"description": "Place an order",
"role": "user",
"payment": "fixed"
},
{
"command": ["orders", "refunds", "create"],
"endpoint": "/orders/{{$id}}/refunds",
"method": "POST",
"description": "Refund an order",
"role": "admin",
"payment": "free"
}
]
}$npx interagentic popcorn.club orders create --qty 2✓ ord_8fJ2 — 2 × Classic, $9.00 $npx interagentic popcorn.club orders refunds create \ --id ord_8fJ2 --reason damaged✓ refunded $9.00A partial chain is help, not an error
interagentic popcorn.club orders lists what lives under orders. You can explore a service you have never seen without opening its docs.
Shell completion falls out for free
Each segment is its own argv token, so completion works the way it does for every other CLI on the machine.
A typo never reaches your API
The client resolves the whole chain against your manifest before sending anything. An unknown command exits with the closest matches, and no request is made.
Action detail
One file per action: its arguments, and where they go.
Arguments are declared once, with types and help text. Templates say where each one lands in the request — so an API whose shape does not match its arguments one-to-one can be described exactly as it is.
{
"command": ["orders", "refunds", "create"],
"endpoint": "/orders/{{$id}}/refunds",
"method": "POST",
"description": "Refund an order",
"role": "admin",
"payment": { "mode": "free" },
"args": ["id"],
"params": [
{ "name": "id", "label": "Order", "required": true,
"description": "The order to refund",
"type": { "type": "string" } },
{ "name": "reason", "label": "Reason", "required": true,
"description": "Why it is refunded",
"type": { "type": "string", "enum": ["damaged", "late", "other"] } },
{ "name": "amount", "label": "Amount", "required": false,
"description": "Partial refund. Omit to refund in full.",
"type": { "type": "number" } },
{ "name": "notify", "label": "Notify", "required": false,
"description": "Email the customer",
"type": { "type": "boolean" } }
],
"request": {
"query": { "notify": "{{$notify}}" },
"body": {
"reason": "{{$reason}}",
"refund": { "amount": "{{$amount}}" }
}
}
}$npx interagentic popcorn.club orders refunds create \ ord_8fJ2 --reason damaged --amount 4.5 --notify✓ refunded 4.50POST /api/v1/orders/ord_8fJ2/refunds?notify=true
Authorization: Bearer <agent jwt>
Content-Type: application/json
{ "reason": "damaged", "refund": { "amount": 4.5 } }Templates
{{$name}}value- The argument's value. Alone in a string it keeps its type — the number 4.5, not the string "4.5". An optional argument that is not given removes its key entirely.
{{$name.a.b}}path- A path inside a structured argument given as JSON, such as
{{$address.city}}. requestoptional- Leave it out and arguments are sent under their own names — in the body for POST, in the query for GET. Most actions need no templates at all.
Rendering is negotiated
Clients append?humanReadable=true by default. If your handler answers text/markdown, the CLI renders it as formatted terminal output; with --json the parameter is dropped and you get your normal JSON. One endpoint, two audiences.Generation
If you publish OpenAPI, you are most of the way there.
The mapping is mechanical. Everything a manifest needs is already in an OpenAPI document — even the command names, which fall out of your paths and methods.
paths:
/orders:
post:
summary: Place an order
requestBody:
content:
application/json:
schema:
type: object
required: [qty]
properties:
qty:
type: integer
minimum: 1What OpenAPI does not carry is pricing and the human-facing labels. Those are the only fields you write by hand.
Every protocol here is a spec you can implement yourself.
There is no gatekeeper. Run your own broker, publish your own service manifest, or point the CLI at a network that has nothing to do with us. The specs are the product.