> For the complete documentation index, see [llms.txt](https://archer-bot.gitbook.io/archer.bot/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://archer-bot.gitbook.io/archer.bot/publish-on-archer/contributing-intents.md).

# Contributing an Intent (SDK & CLI)

The `@archerprotocol/sdk` package turns any service you run into an Archer intent: discoverable by natural-language search, invocable by users and agents, and monetized at a rate you set. This guide takes you from an empty folder to a live, published intent.

If you want the underlying wire contract (raw envelope, signature canonicalization, schema rules), read [Publishing a Partner Intent](/archer.bot/publish-on-archer/publishing-a-partner-intent.md). This page is the fast path that the SDK and CLI build on top of it.

## What the SDK gives you

Three things, one install:

1. **Verify** inbound Archer requests in one line. No API key needed for this; the SDK fetches Archer's public key for you.
2. **Respond** with the generic response envelope using typed builders, so the shape is correct by construction.
3. **Publish and manage** your intent from the `archer` command-line tool: create, activate, edit, and read telemetry.

```bash
npm install @archerprotocol/sdk            # Node / TypeScript
pip install "archerprotocol-sdk[fastapi]"  # Python
```

The Python package (`archerprotocol-sdk`, imported as `archer_sdk`) mirrors the Node builder surface, and both are held to the same shared test vectors, so an envelope built in Python is byte-identical to one built in TypeScript. The examples below use TypeScript; see [Building in Python](#building-in-python) for the same flow in Python. If you only need to verify inbound signatures, the standalone `archer-verify` package covers that on its own.

## Step 1: Build a read intent

A read intent returns data. Here is a price service using Express. `createArcherIntent` verifies the request before your handler runs, and `archer.render` returns text plus an optional embed that renders in the Archer GUI.

```ts
import express from 'express'
import { createArcherIntent, archer } from '@archerprotocol/sdk'

const app = express()
app.use(express.json())

app.post('/price', createArcherIntent({
  handler: async ({ args }) => {
    const price = await lookupPrice(args.ticker)
    return archer.render({
      text: `${args.ticker} is $${price.usd}`,
      embed: archer.embed.metric({ label: args.ticker, value: `$${price.usd}` }),
    })
  },
}))

app.get('/health', (_req, res) => res.json({ ok: true }))
app.listen(3000)
```

That is the whole service. `createArcherIntent`:

* Verifies the signed request. An unsigned call returns `401`, a forged one returns `403`.
* Parses the request and hands your handler `{ args, caller, requestId, userId, ... }`.
* Wraps your return value in the response Archer expects.

The embed builders (`archer.embed.chart`, `metric`, `table`, `kv`, `alert`, `image`) mirror the render kinds Archer supports, so your data shows up as a chart or metric rather than raw text.

## Step 2: Understand the response envelope

Every invocation returns an envelope with up to four optional arms. Archer maps each arm to a stage, and skips the stages you leave out.

| Arm                | Stage     | Use it for                                                                 |
| ------------------ | --------- | -------------------------------------------------------------------------- |
| `data`             | Render    | reads, quotes, analytics (or return `archer.render(...)` for text + embed) |
| `cost`             | Settle    | your fee for this call (`flat`, `percent`, `x402`, `embedded`, or `none`)  |
| `authorizations[]` | Authorize | a transaction or signature the user must approve                           |
| `record`           | Record    | a durable outcome the user owns, such as a position                        |

A read intent uses only `data` (via `archer.render`). An intent that moves funds uses `authorizations[]` and `record`. You never build this shape by hand; the `archer.*` builders validate and coerce it for you.

## Step 3: Declare and publish

Scaffold a manifest, then publish it with the CLI.

```bash
npx @archerprotocol/sdk init
```

This writes `archer.intent.json` (plus a JSON Schema for editor validation). Fill in the fields:

```jsonc
{
  "$schema": "./archer.intent.schema.json",
  "publicIdentifier": "@MyBot",
  "name": "My Bot",
  "description": "One line describing what your intent does.",
  "category": "analytics",
  "capabilities": ["RETURNS_DATA"],
  "bindingType": "REST",
  "toolEndpoint": "https://my.app/price",
  "healthEndpoint": "https://my.app/health",
  "parametersSchema": { "type": "object", "properties": { "ticker": { "type": "string" } } },
  "feeRate": 0.01,
  "payoutAddresses": { "evm": "0x..." },
  "sampleQueries": ["price of eth", "eth price"],
  "requiresCaller": false
}
```

`capabilities` are the generic effect classes your intent declares: `RETURNS_DATA`, `MUTATES_USER_FUNDS`, or `EXTERNAL_SIDE_EFFECT`. `sampleQueries` feed semantic search, so write the phrases a user would type.

Authenticate the CLI, then publish:

```bash
# Sign in through the browser (approve the CLI in the page that opens)...
archer auth login
# ...or paste a publish-scoped key from the developer dashboard (no browser, good for CI):
archer auth login --token arch-...

archer publish            # reads archer.intent.json, upsert by handle
archer activate @MyBot    # probes your endpoint, then makes it public
```

A published intent starts **inactive**: hidden from discovery, but you can still invoke it yourself while you test. `archer activate` probes your endpoint (it must reject unsigned and forged requests) and then flips it live.

Track it over time:

```bash
archer list
archer status @MyBot
archer stats @MyBot --since 30d      # hits and fees over time
```

Add `--json` to any command for machine-readable output, and `--base-url <url>` to target a specific deployment.

### Manage from code

Everything the CLI does is also available programmatically through the `Archer` client, for dashboards, CI, or a control plane of your own:

```ts
import { Archer } from '@archerprotocol/sdk'

const archer = new Archer({ apiKey: process.env.ARCHER_API_KEY })

await archer.publishIntent(input)
await archer.updateIntent(id, input)
await archer.toggleIntent(id, isActive)
await archer.feesTimeseries(intentId, 30)
```

## MUTATES\_FUNDS deep-dive

An intent that acts on the user's funds returns a transaction for them to sign, and records the durable outcome. Three pieces make this work.

### 1. Read the caller

When your intent needs the user's wallet, declare `"requiresCaller": true` in the manifest. Archer then injects the caller into the signed request, and you read it with `getArcherCaller`:

```ts
import { requireArcherCaller } from '@archerprotocol/sdk'

const caller = requireArcherCaller(args) // { evmAddress, svmAddress, archerFeeRate }
```

The caller rides inside the signed `args`, so it is verified along with everything else. `requireArcherCaller` throws a clear error if the intent was published without `requiresCaller: true`.

### 2. Return the transaction and the record

Compose the transaction, then return it as an `authorization` alongside a `record` describing the durable outcome:

```ts
import { createArcherIntent, archer, requireArcherCaller } from '@archerprotocol/sdk'

export const deposit = createArcherIntent({
  handler: async ({ args }) => {
    const caller = requireArcherCaller(args)
    const tx = await composeDeposit(caller.evmAddress, args.amount)

    return archer.envelope({
      cost: archer.cost({ model: 'embedded', archerFeeMicro: feeMicro }),
      authorizations: [
        archer.authorization({ type: 'tx', namespace: 'evm', label: 'Deposit 100 USDC', payload: tx }),
      ],
      record: archer.record({
        recordKind: 'position',
        basisMicro: amountRaw,
        groupKey: vaultAddress,
        viewUrl: 'https://my.app/positions/123',
        exitInstructions: { endpoint: '/withdraw', selector: 'redeem' },
        disclosure: { custody: 'SELF', exitModel: 'PERMISSIONLESS' },
      }),
    })
  },
})
```

* `authorizations[]` is what the user signs. An EVM transaction is one authorization `type`, so a swap, a deposit, or a booking payment all ride the same rail.
* `record` is the durable outcome the user owns. Store opaque, self-defined descriptors. `exitInstructions` and `viewUrl` are yours to shape. A position is one `recordKind`, not a special case.
* `label` on each authorization is the human-readable line the user sees before signing. Make it specific.

### 3. Disclose custody and exit

The `disclosure` field states, up front, who holds the funds (`custody`) and how the user gets out (`exitModel`). Archer surfaces this before the user signs, so they understand the trust model before committing.

### Fees

The `cost` arm carries your fee model. Archer bundles your fee, the on-chain gas estimate, and its own platform fee into one figure the user approves before signing. Unknown fee models degrade gracefully rather than failing, so a model Archer does not recognize is treated as data. See [Pricing & Fees](/archer.bot/build-with-archer/pricing.md) for how the platform fee works.

## Building in Python

Everything above works the same way from Python. Install the package with the FastAPI extra:

```bash
pip install "archerprotocol-sdk[fastapi]"
```

`mount_archer_tools` gives you the one-call handler: it verifies the signed request (via `archer-verify`, installed as a dependency), hands your handler the context, and wraps your return in the response Archer expects.

```python
from fastapi import FastAPI
from archer_sdk import archer, require_archer_caller, BadInputError
from archer_sdk.fastapi import mount_archer_tools

app = FastAPI()

async def price(ctx):
    quote = await lookup_price(ctx.args["ticker"])
    return archer.render(
        text=f"{ctx.args['ticker']} is ${quote['usd']}",
        embed=archer.embed.metric(label=ctx.args["ticker"], value=f"${quote['usd']}"),
    )

async def deposit(ctx):
    caller = require_archer_caller(ctx.args)
    amount = ctx.args.get("amount")
    if not amount:
        raise BadInputError('amount is required (e.g. "1.5")')
    tx = await compose_deposit(caller["evmAddress"], amount)
    return archer.envelope(
        cost=archer.cost(model="embedded", archer_fee_micro=fee_micro),
        authorizations=[
            archer.authorization(type="tx", namespace="evm",
                                 label=f"Deposit {amount} USDC", payload=tx)
        ],
        record=archer.record(
            record_kind="position",
            basis_micro=amount_raw,
            disclosure={"custody": "SELF", "exitModel": "PERMISSIONLESS"},
        ),
    )

mount_archer_tools(app, {"/price": price, "/deposit": deposit})
```

Your handler receives `ctx.args`, `ctx.caller`, `ctx.request_id`, `ctx.intent_definition_id`, and `ctx.user_id`. Raise `BadInputError` (returns `400`) or `UpstreamError` (returns `503`) for clean failures. Builder keyword arguments are snake\_case; the envelope Archer receives carries the same camelCase keys as the Node SDK.

Publishing works exactly as in Step 3: the `archer` CLI runs without a Node project via `npx @archerprotocol/sdk`, so `archer init`, `archer publish`, and `archer activate` are the same commands whichever language serves your endpoint.

## Where to go next

* [Publishing a Partner Intent](/archer.bot/publish-on-archer/publishing-a-partner-intent.md) for the raw envelope, signature canonicalization, and parameter-schema rules.
* [Partner REST dispatch contracts](/archer.bot/publish-on-archer/partner-dispatch-contracts.md) for exactly what arrives in `args` on each dispatch path.
* [Partner intent telemetry](/archer.bot/publish-on-archer/partner-intent-telemetry.md) for the metrics behind `archer stats`.
* [Pricing & Fees](/archer.bot/build-with-archer/pricing.md) for the fee model and payouts.
