> 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/for-builders/publishing-a-partner-intent.md).

# Publishing a Partner Intent

> **Status: live on the `test` environment.** The dispatch path and the `@handle` LLM exposure ship on `archer-api-test.up.railway.app`. The first dogfood partner, `@TokenAnalyzer`, is wired end-to-end and is used as the worked example throughout this guide. Promotion to production rides with the [Phase 2B partner-experience milestone](https://archerprotocol.com).

This page takes you from "I have an MCP server or REST API" to "Alice can call me by name" in roughly 30 minutes. If you have an HTTPS endpoint that speaks JSON, you already have 90% of what you need.

## Audience

This guide is for the **supply side** of the Archer marketplace: developers publishing executable intents that Alice (a human user, or her agent) invokes in natural language. If instead you want to *consume* Archer from your own agent or IDE, read the [Quickstart](/archer.bot/for-builders/quickstart.md) and the [MCP Server overview](/archer.bot/for-builders/overview.md). Those are different surfaces.

## Self-serve publishing

You can publish your partner intent two ways. Both routes hit the same backend service, enforce the same capability-mismatch validator, and share the same per-Partner rate limit (5 publishes / hour).

### Web form (recommended for humans)

1. Sign in at [app.archerprotocol.com](https://app.archerprotocol.com).
2. Visit `/developer/intents/new`.
3. Fill in the nine-field form: `@handle`, name, description, category, capabilities (multi-select; `MUTATES_USER_FUNDS` auto-switches the category to a write category), binding (`REST` or `MCP_HTTP`), endpoint URL, JSON parameters schema, and fee rate.
4. Click **Test endpoint** to round-trip a signed envelope against your URL before publishing. For both `REST` and `MCP_HTTP` this runs a signed round-trip **and** a negative-auth probe (an unsigned request + a forged-signature request) to confirm your endpoint rejects unauthenticated traffic — the check that gates activation.
5. Click **Publish**. Your `IntentDefinition` row is created as **inactive**.
6. Click **Activate** once your endpoint round-trips correctly. Activation is reversible from `/developer/intents/[id]`.

Your `Partner` profile is auto-created from your Privy user the first time you publish. You can view it read-only at `/developer/partner/settings`; editing the profile is on the follow-up roadmap.

### MCP API (for agents publishing programmatically)

Provision a developer API key with the **`publish`** scope at `/developer/api-keys`. The Archer MCP server then exposes six partner tools:

| Tool                            | Purpose                                               |
| ------------------------------- | ----------------------------------------------------- |
| `partner_publish_intent`        | Create one new `IntentDefinition` row.                |
| `partner_publish_intent_bundle` | Create N rows atomically (multi-capability partners). |
| `partner_test_endpoint`         | Round-trip a signed envelope against your URL.        |
| `partner_set_active`            | Toggle `isActive`.                                    |
| `partner_list_intents`          | List your Partner's intents.                          |
| `partner_archive_intent`        | Soft-delete (sets `archivedAt`).                      |

All six tools route through the same `partner-intent-service` module as the web form: the capability-mismatch validator, the rate limiter, and the `findOrAutoCreatePartner` flow are shared, so the surface you publish through does not change behavior — only ergonomics.

## Overview

A **partner intent** is one row in Archer's `IntentDefinition` table that points at your endpoint. Once that row exists:

1. Alice types something like `use @TokenAnalyzer to look up ETH price`.
2. Archer's `@handle` parser matches `@TokenAnalyzer` to your row.
3. Archer's LLM agent picks your tool (its signature is built from your row's `parametersSchema` and `description`), fills in `{ ticker: "ETH" }`, and the agent loop calls your endpoint via the `RemoteToolAdapter`.
4. Archer POSTs a **signed envelope** to your `toolEndpoint`. You verify the signature, run your business logic, and return JSON.
5. Archer relays your result back to Alice, attributes the response with a "via @TokenAnalyzer" badge, and settles billing.

You never see Alice's wallet or Archer's internals. Your endpoint sees a request body it can verify cryptographically, an `args` object that matches your declared `parametersSchema`, and an `Idempotency-Key` header for safe retries.

> **Verifying that signature is mandatory, not optional.** Your endpoint is a public URL — without verification anyone who finds it can call it directly, run up your provider costs, and bypass Archer's payment and attribution. At activation Archer probes your endpoint with an unsigned/forged request and **refuses to activate** unless you reject it with `401`/`403`. See [Step 3: Verify the signature](#step-3-verify-the-signature) and the [dispatch contracts](/archer.bot/for-builders/partner-dispatch-contracts.md#verifying-archers-signature-is-mandatory).

### How Alice reaches you (web, Search, and Archer MCP)

Alice always uses the **same Archer backend** — ranking, `@handle` parsing, billing, and dispatch. She does **not** pick REST vs MCP; your `bindingType` only changes **how your server receives** the signed call.

| Alice surface       | What she does                                                                   | What Bob sees                            |
| ------------------- | ------------------------------------------------------------------------------- | ---------------------------------------- |
| Web chat            | Types `@YourHandle …` or natural language that routes to you                    | Same signed dispatch as below            |
| Search (`/s`)       | Discovers via embeddings; **Run** uses flat-fee flow when configured            | Same dispatch after payment              |
| Archer's MCP server | External agents call `invoke_partner_message` with `@YourHandle` in the message | Same dispatch — not a second integration |

You do **not** need separate configs for "web vs MCP." One `IntentDefinition` row, one binding, one endpoint.

### Your `@handle` is permanent

| Field                                               | Set when | Editable after publish?       |
| --------------------------------------------------- | -------- | ----------------------------- |
| **`publicIdentifier` (`@handle`)**                  | Publish  | **No** — immutable            |
| **`name` (display label)**                          | Publish  | Yes (edit form)               |
| **`description`, endpoint, fee, sample queries, …** | Publish  | Yes (where the form allows)   |
| **`bindingType` (`REST` / `MCP_HTTP`)**             | Publish  | **No** — locked after publish |
| **`capabilities`, `category`**                      | Publish  | **No** — locked after publish |

To rebrand a handle, publish a **new** intent (new `@handle`) and archive the old row. For `MCP_HTTP`, your MCP tool name must match `@handle` at publish time — renaming the tool later without a new intent will break dispatch.

### Choosing REST vs MCP (and when both is redundant)

|               | **REST** (default)                                                                                                          | **MCP\_HTTP**                                                                                                                |
| ------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **You run**   | `POST` HTTPS route returning the response envelope                                                                          | Streamable-HTTP MCP server                                                                                                   |
| **Best for**  | New partners, simple handlers                                                                                               | You **already** operate MCP                                                                                                  |
| **Verify**    | Signed JSON **body**                                                                                                        | Reconstruct envelope from `X-Archer-*` headers + tool `args`                                                                 |
| **Reference** | Node + Python REST verifiers in [`archer-partner-examples`](https://github.com/Archer-Laboratories/archer-partner-examples) | MCP header-verification notes in [`archer-partner-examples`](https://github.com/Archer-Laboratories/archer-partner-examples) |

**Best practices:**

1. **One binding per product** — pick REST unless you already ship MCP.
2. **Do not offer REST and MCP for the same service** unless you want **two marketplace listings** (two `@handle`s, two fees, two rows). One intent = one binding.
3. **MCP tool name = `@handle`** exactly (e.g. tool `@SwapBot` for handle `@SwapBot`). Register **without** an input schema so args stay byte-faithful for verification.

See also [Partner intent telemetry](/archer.bot/for-builders/partner-intent-telemetry.md) for what the developer portal charts mean today.

### What gets billed

A single partner invocation produces three line items, **bundled into one charge to Alice**:

| Line item                                                                        | Set by                                       | Paid to                                                  |
| -------------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------------------- |
| Your rate (per-call, per-action, or pricing-shape for delegation)                | You, via `feeRate` + optional `pricingShape` | Your `payoutAddresses` on the chain where the fee landed |
| Archer's TVR platform fee (basis points on routed value, varies by Alice's tier) | Archer pricing                               | Archer treasury                                          |
| On-chain gas (pass-through, if your intent settles a transaction)                | The network                                  | Validators / sequencer                                   |

You quote your rate in [`feeRate`](#feerate-and-pricingshape). Archer's TVR fee and Alice's per-call CU cost are handled by [Archer's pricing tiers](/archer.bot/for-builders/pricing.md); you do not configure them.

## Step 1: Pick a `bindingType`

Two ways to plug in. Pick whichever matches what you already have.

### `REST` (recommended for new integrations)

You expose `POST <your-url>/tool`. Archer POSTs a signed JSON body; you reply with a JSON body. This is the fastest path if you do not already run an MCP server.

### `MCP_HTTP` (recommended if you already run an MCP server)

You already have a [Model Context Protocol](https://modelcontextprotocol.io) server reachable over Streamable HTTP. Archer connects as an MCP client and calls `callTool(<your-public-identifier>, args)`. Your tool name on the MCP side must equal your row's `publicIdentifier` verbatim (including the leading `@`). Archer signs the **same envelope** as REST and forwards the signed fields as HTTP headers on every `callTool`: `X-Archer-Signature`, `X-Archer-Timestamp`, `X-Archer-Request-Id`, `X-Archer-Intent-Id`, and `X-Archer-User-Id`. Because MCP has no request body envelope, you reconstruct the canonical payload `{ requestId, intentDefinitionId, userId, args, timestamp }` from those headers plus the JSON-RPC tool `args`, then verify it with the exact same Ed25519 + canonical-JSON primitive as REST (see Step 3). Verify on the `callTool` request. **Register your MCP tool without an input schema** so `args` reach your handler un-coerced — a type-coerced argument would change the canonical bytes and break verification. The MCP header-verification reference lives in [`archer-partner-examples`](https://github.com/Archer-Laboratories/archer-partner-examples).

For the rest of this guide, the worked example uses `REST`. The signature verification semantics are identical for `MCP_HTTP`; only the transport and the payload-reconstruction source (headers + args vs request body) differ.

## Dispatch paths and `args` (read this before implementing)

Archer uses **one REST `POST`** per invocation, but **two ways** `args` get filled:

| Path                                                           | `args` shape                                              | Who parses Alice's language |
| -------------------------------------------------------------- | --------------------------------------------------------- | --------------------------- |
| **AgentLoop**                                                  | Structured JSON from your `parametersSchema` (LLM-filled) | Your schema + LLM           |
| **Flat-fee** (`feeRateUnit` = `flat_stablecoin` / `flat_usdc`) | Opaque `{ "query": "…" }` only (handle stripped)          | **You** (Bob)               |

Flat-fee adds pay-before-dispatch (EIP-3009 USDC on Base Sepolia). REST verification and response format are unchanged.

**Technical deep-dive:** [Partner REST dispatch contracts](/archer.bot/for-builders/partner-dispatch-contracts.md) — POST-only contract, portfolio/context limitations, and the future context-payload direction.

## Step 2: The request envelope

When `@YourHandle` is invoked, Archer makes one HTTP request:

```
POST https://your-host.example.com/your-slug/tool
Content-Type: application/json
Idempotency-Key: <requestId>
X-Archer-Request-Id: <requestId>
X-Archer-Intent-Id: <IntentDefinition.id>
X-Archer-Signature: <base64-encoded Ed25519 signature>
X-Archer-Timestamp: <unix epoch ms>
```

Body:

```json
{
  "requestId": "8c1c6e57-9c52-4f8f-9d3e-2a4f4b3a0d2a",
  "intentDefinitionId": "cldd2k0qp0001r6tq8r1v1k0a",
  "userId": "cldd3a0p10001r6tq3g6c9bj1",
  "args": { "ticker": "ETH" },
  "timestamp": 1715712345678,
  "archerSignature": "MEUCIQD..."
}
```

| Field                | Type          | Meaning                                                                                                                                     |
| -------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `requestId`          | UUID string   | Unique per invocation. Also sent in `Idempotency-Key`. Use as your dedupe key on retries.                                                   |
| `intentDefinitionId` | string        | Your `IntentDefinition.id`. Constant across requests for the same intent.                                                                   |
| `userId`             | string        | Opaque-to-you identifier for the calling user. Treat as a pseudonymous handle; do not assume it is an email, wallet, or anything else.      |
| `args`               | object        | Validated against your `parametersSchema` before Archer sends it.                                                                           |
| `timestamp`          | number        | Unix epoch milliseconds at signing. Use for replay-window enforcement (Archer enforces a 5-minute skew by default; mirror it on your side). |
| `archerSignature`    | base64 string | Ed25519 signature over canonical JSON of the envelope **without this field**. See next section.                                             |

## Step 3: Verify the signature

The signature is Ed25519 over the canonical JSON of every field except `archerSignature`. Canonicalization rules:

* Object keys sorted lexicographically at every nesting level
* Arrays preserve insertion order
* `undefined` values dropped
* `null` values kept
* No whitespace

Bytes on the wire must match exactly. A single stray space invalidates the signature.

### Fetch Archer's public key

Archer publishes its Ed25519 SPKI public key as a PEM at:

* **Test**: `https://archer-api-test.up.railway.app/.well-known/archer-public-key`
* **Production**: `https://api.archerprotocol.com/.well-known/archer-public-key`

The endpoint sets `Cache-Control: public, max-age=3600`. Cache the result for an hour. On refresh failure, keep serving with the stale value and log a warning rather than rejecting traffic.

### Reference verifier

Copy-paste verifiers (start here — smallest surface):

* [**`archer-partner-examples`**](https://github.com/Archer-Laboratories/archer-partner-examples) — minimal REST verifiers in Node (`node:crypto`) and Python (`cryptography`). No framework lock-in.
* **MCP\_HTTP** — [mcp-verifier-notes.md](https://github.com/Archer-Laboratories/archer-partner-examples/blob/main/mcp-verifier-notes.md) for production-shaped `X-Archer-*` header verification.

The Node/TypeScript REST core is \~80 lines:

### Parameters schema examples <a href="#parameters-schema-examples" id="parameters-schema-examples"></a>

The **Parameters schema** field on the publish form is JSON Schema describing what Archer may place in `envelope.args`. Which shape you use depends on how Alice invokes you — see [Partner REST dispatch contracts](/archer.bot/for-builders/partner-dispatch-contracts.md).

**Flat-fee / opaque `query` (DemoAnalyst-style)** — when `feeRateUnit` is `flat_stablecoin` / `flat_usdc`, Archer usually sends only `{ "query": "…" }` after payment. Your server parses natural language into your own API:

```json
{
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "description": "User text after the @handle is stripped"
    }
  },
  "required": ["query"]
}
```

Matches the dogfood `@DemoAnalyst` / `@DemoAnalystMcp` partners.

**AgentLoop / structured (TokenAnalyzer-style)** — when the LLM routes via your `parametersSchema`, fields are filled from Alice's message:

```json
{
  "type": "object",
  "properties": {
    "ticker": {
      "type": "string",
      "description": "Token symbol, e.g. ETH, SOL, BTC"
    },
    "days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 365,
      "description": "Chart lookback in days"
    }
  },
  "required": ["ticker"]
}
```

Use descriptive property names and explicit `required` arrays so the router LLM can populate `args` reliably. The portal **Test endpoint** uses a synthetic probe; a schema mismatch may show as a partner error even when signatures verify.

### Reference verifier (REST snippet)

Until published npm/PyPI packages ship, the inline REST verifier below matches the Node verifier in [`archer-partner-examples`](https://github.com/Archer-Laboratories/archer-partner-examples):

```ts
// shared/signature-verifier.ts (abbreviated)
import { verify, createPublicKey } from 'node:crypto';

export function canonicalizeJson(value: unknown): string {
  return JSON.stringify(sortKeys(value));
}

function sortKeys(value: unknown): unknown {
  if (value === null || typeof value !== 'object') return value;
  if (Array.isArray(value)) return value.map(sortKeys);
  const out: Record<string, unknown> = {};
  for (const key of Object.keys(value as Record<string, unknown>).sort()) {
    const v = (value as Record<string, unknown>)[key];
    if (v === undefined) continue;
    out[key] = sortKeys(v);
  }
  return out;
}

export function verifyEnvelope(envelope: Envelope, publicKeyPem: string): boolean {
  try {
    const { archerSignature, ...rest } = envelope;
    const sigBuf = Buffer.from(archerSignature, 'base64');
    if (sigBuf.length !== 64) return false; // Ed25519 sigs are exactly 64 bytes
    const key = createPublicKey({ key: publicKeyPem, format: 'pem' });
    const payload = Buffer.from(canonicalizeJson(rest), 'utf8');
    return verify(null, payload, key, sigBuf);
  } catch {
    return false;
  }
}
```

You MUST **reject** (return `401`/`403`) — not merely log — if:

* `archerSignature` is missing or malformed (not 64 raw bytes after base64 decode)
* The signature does not verify against Archer's public key
* The `timestamp` is more than 5 minutes off your local clock (replay protection)
* The `Idempotency-Key` header is present and does not equal `requestId`

> **Going live is gated on this.** When you publish, your intent is created **inactive** (testable only from your own account). Activating it (going live to all users) re-runs a **negative-auth probe** that sends an unsigned request and a forged-signature request to your endpoint. Activation is **refused** unless your endpoint rejects **both** with `401`/`403`. An endpoint that accepts unsigned/forged requests is directly callable by anyone, bypassing Archer billing — so it can publish, but it cannot go live until it verifies. A flaky/inconclusive probe (timeout, network error) is a "retry," not a pass.

Reject with `400` for shape problems (missing fields, wrong types). Reject with `503` if you could not fetch the Archer public key and have no cached copy.

## Step 4: The response envelope

Reply with one of two shapes.

**Success**:

```json
{
  "success": true,
  "result": { "ticker": "ETH", "priceUsd": 2532.15, "priceChange24hPct": -0.84 },
  "providerCostMicro": 0
}
```

**Failure**:

```json
{
  "success": false,
  "error": { "code": "BAD_INPUT", "message": "args.ticker is required" }
}
```

| Field               | Required when    | Meaning                                                                                                                                                                         |
| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success`           | always           | Boolean.                                                                                                                                                                        |
| `result`            | `success: true`  | Free-form JSON. Archer relays it to Alice; the LLM sees it as the tool result. Shape should match your declared `outputSchema`.                                                 |
| `error.code`        | `success: false` | Short machine-readable code (e.g. `BAD_INPUT`, `UPSTREAM`, `RATE_LIMITED`).                                                                                                     |
| `error.message`     | `success: false` | Human-readable message. Shown to Alice when surfaced.                                                                                                                           |
| `providerCostMicro` | optional         | Your reported cost in micro-USD for this request. Lands in Archer's `providerCostMicro` billing bucket and is included in Alice's bundled charge. Defaults to `0` when omitted. |

## Step 5: Categories and retry policy

The `category` you pick on your `IntentDefinition` row controls how Archer's agent reasons about your intent and how the `RemoteToolAdapter` handles transient failures.

### Write categories (no automatic retry)

Categories that move funds or write to chain:

```
SWAP, SEND, BRIDGE, EXECUTE, STAKE, MINT, TRANSFER, BURN, CLAIM
```

For these:

* Archer **never retries** on transient errors. The first attempt is the only attempt.
* The LLM treats your tool as `requiresConfirmation: true`. When the agent decides to call you, the loop **halts before dispatching**. Alice sees a confirmation prompt; only when she approves does Archer call your endpoint.
* The same `requestId` is used for the eventual dispatch, so you can store state during the proposal phase (rare, but supported).

### Idempotent categories (1 retry on 5xx)

Read-shaped categories such as `PRICE`, `BALANCE`, `QUERY`, `ANALYZE`, `OTHER`. For these, Archer retries once on transient 5xx or network errors, **with the same `requestId`**. Your endpoint must dedupe on `requestId` (typically by caching the prior response for \~60 seconds) or be naturally idempotent.

### Timeout

Default 30 seconds. Override per-intent with `IntentDefinition.timeoutMs`. Archer cancels the in-flight request when the timeout elapses; treat a cancelled request as if it never happened.

## Step 6: `feeRate`, `payoutAddresses`, and `pricingShape`

### `feeRate`

A single number on your `IntentDefinition` row. Two common shapes:

* **Per-call price** (e.g. `1` for "1 USDC per request"). Use for read-shaped intents that don't settle a transaction.
* **Basis points on routed value** (e.g. `15` for 0.15% of TVR). Use for intents that execute swaps, bridges, or other value-moving operations.

`feeRate = 0` is valid and means "free during testing/dogfood." `@TokenAnalyzer` ships with `feeRate = 0` for the PoC.

### When does my fee actually get charged?

**Short version: only after you return a successful response.** Archer never holds or reserves your `feeRate` upfront. If your endpoint errors, times out, or returns `success: false`, Alice is not charged your fee — full stop.

The lifecycle for a single invocation:

1. **Reservation (start of turn)**. Archer reserves Alice's credit balance for her share of Archer's infra + LLM costs only — `providerCostMicro` (your fee bucket) is **0** at this point. Your fee is **not** held against her balance during dispatch.
2. **Dispatch**. Archer POSTs the signed envelope to your `toolEndpoint` (REST) or invokes your MCP tool (`MCP_HTTP`).
3. **Settlement (your response decides the outcome).**

| Your response                                                                  | What Archer charges Alice                                                                        |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| `success: true, providerCostMicro: N`                                          | Settle `N` micro-USD into your `payoutAddresses` + record on the CU ledger.                      |
| `success: true, providerCostMicro: 0` (or omitted)                             | Free dispatch — nothing charged on your behalf.                                                  |
| `success: false, error: { code, message }`                                     | **Your fee is never charged.** Archer refunds the infra/LLM reservation back to Alice's balance. |
| HTTP 5xx, timeout, or network failure                                          | **Your fee is never charged.** Same refund path as `success: false`.                             |
| Malformed envelope (sig fails, JSON parse error, non-2xx with `success: true`) | Treated as failure — your fee is never charged.                                                  |

This is "charge-on-success, end-of-turn" by design. Two practical consequences worth keeping in mind:

* **A partial / wrong / hallucinated result that you still mark as `success: true` WILL be charged to Alice.** Archer trusts your success signal at the dispatch layer. Quality is enforced separately via the response-rating surface and the marketplace ranking algorithm's success-rate weight — both feedback loops on you, not refund mechanisms.
* **Don't pre-emptively reserve resources on your end against the `feeRate` before returning success.** If you build a flow where your fee covers costly upstream work, do that work BEFORE you decide whether to return `success: true`, then bake the cost into `providerCostMicro` when you do.

Implementation references (Archer side, for the curious):

* `api/src/billing/credit-gate.ts:40` — `infraOnlyComponents()` deliberately initializes `providerCostMicro: 0` for every reservation.
* `api/src/agent/ToolExecutor.ts:680–690` — settle uses your partner-reported `providerCostMicro` on success; refund path runs on failure with `providerCostMicro = 0` (i.e. nothing to refund on the fee bucket, infra/LLM restored).

### `payoutAddresses`

A JSON object mapping chain namespace to receiver address. Fees collected on a given chain stream to that chain's configured address.

```json
{
  "evm": "0xBobEvmReceiver",
  "svm": "BobSolanaBase58Receiver",
  "evm:base": "0xBobOnBaseSpecifically",
  "evm:arbitrum": "0xBobOnArbitrumSpecifically"
}
```

Keys can be a namespace alone (`evm`, `svm`) or `namespace:chain` for per-chain overrides. When a fee lands on Base, Archer prefers `evm:base` over the generic `evm` entry. New namespaces (Cosmos, Bitcoin, etc.) plug in without a schema change.

### `pricingShape` (delegation-tier intents only)

If your intent custodies or manages Alice's funds (a vault, a yield optimizer, an actively-managed strategy), you **must** publish your pricing shape upfront. Archer's UI surfaces this to Alice before she contributes capital.

```json
{
  "managementFeeBps": 200,
  "performanceFeeBps": 2000,
  "lockupSeconds": 0,
  "redemption": "anytime",
  "highWaterMark": true
}
```

You can add custom fields freely; the marketplace UI renders the standard fields above plus a key/value table for everything else.

For one-shot intents (swaps, queries, analysis), leave `pricingShape` `NULL`.

## Worked example: `@TokenAnalyzer`

The first dogfood partner is `@TokenAnalyzer`, a thin wrapper around CoinGecko. The whole service is roughly 100 lines of Express plus the shared verifier. The public [`archer-partner-examples`](https://github.com/Archer-Laboratories/archer-partner-examples) repo carries equivalent runnable verifiers; the highlights are below.

### The router

```ts
// token-analyzer router (abbreviated)
import { Router } from 'express';
// verifyEnvelopeMiddleware wraps the Ed25519 verifier from Step 3
// (see the Node verifier in archer-partner-examples).
import { verifyEnvelopeMiddleware } from './archer-verifier.js';

export function createTokenAnalyzerRouter(): Router {
  const router = Router();
  const client = new CoinGeckoClient({ apiKey: process.env.COINGECKO_API_KEY! });

  router.post('/tool', verifyEnvelopeMiddleware(), async (req, res) => {
    const envelope = req.archerEnvelope!;
    const ticker = readStringArg(envelope.args, 'ticker');
    if (!ticker) {
      return res.status(400).json({
        success: false,
        error: { code: 'BAD_INPUT', message: 'args.ticker is required (string)' },
      });
    }
    try {
      const result = await client.lookupByTicker(ticker);
      return res.status(200).json({ success: true, result, providerCostMicro: 0 });
    } catch (err) {
      // BadInputError → 400, UpstreamError → 503 (Archer retries once on 503 for PRICE)
      // ...
    }
  });

  return router;
}
```

The middleware does the signature work; your business logic touches only `envelope.args` and the request/response JSON.

### The `IntentDefinition` row

```sql
INSERT INTO "IntentDefinition" (
  "id", "publicIdentifier", "name", "description", "category",
  "bindingType", "toolEndpoint", "timeoutMs",
  "parametersSchema", "outputSchema",
  "feeRate", "payoutAddresses", "pricingShape",
  "ownerId", "isActive", "isCoreIntent",
  "verificationStatus", "createdAt", "updatedAt"
) VALUES (
  gen_random_uuid()::text,
  '@TokenAnalyzer',
  'Token Analyzer',
  'Look up live price, market cap, 24h change, and chain platforms for a token by ticker.',
  'PRICE',
  'REST',
  'https://your-service.example.com/token-analyzer/tool',
  30000,
  '{"type":"object","properties":{"ticker":{"type":"string"}},"required":["ticker"]}'::jsonb,
  '{"type":"object"}'::jsonb,
  0,
  NULL,
  NULL,
  '<bob-user-id>',
  true,
  false,
  'UNVERIFIED',
  NOW(), NOW()
);
```

Three things to notice:

1. **`category: 'PRICE'`** is an idempotent category, so transient 5xx errors trigger one retry with the same `requestId`. The CoinGecko wrap is naturally idempotent; no dedupe needed.
2. **`feeRate: 0`** for the PoC. Real partners set this to their per-call price or bps.
3. **`payoutAddresses: NULL`** because `feeRate: 0`. Set this to a real map once you charge.

After inserting, run the partner embedding backfill so the row gets a vector representation:

```bash
# from the api/ repo, against your target environment
npm run backfill:partner-embeddings
```

## Testing against the `test` environment

The `test` environment is the safe place to develop. It is a separate Railway deploy backed by a separate Supabase project; nothing you do on test touches real users or real funds.

### Endpoints

| Resource             | URL                                                                    |
| -------------------- | ---------------------------------------------------------------------- |
| Archer API (test)    | `https://archer-api-test.up.railway.app`                               |
| Archer webapp (test) | `https://archer-web-app-ui-test.up.railway.app`                        |
| Public-key endpoint  | `https://archer-api-test.up.railway.app/.well-known/archer-public-key` |
| Sample partner host  | `https://your-service.example.com`                                     |

### End-to-end check

1. **Stand up your endpoint.** Deploy it anywhere reachable over HTTPS (Railway, Vercel, Fly, etc.). Point it at the test public-key endpoint via the `ARCHER_API_URL` env var.
2. **Get an Archer engineer to insert your `IntentDefinition` row.** Until the self-serve publish form is generally available, send your row payload (the SQL above with your values) to your Archer contact. They will insert it into the test Supabase and run the embedding backfill.
3. **Provision a test API key** (optional, only if you want to drive Archer programmatically). Sign in at `https://archer-web-app-ui-test.up.railway.app/developer`, create an API key with the scopes you need, and use it in `x-api-key` or `Authorization: Bearer ...` headers against the test API. See the [Developer Portal](/archer.bot/for-builders/developer-portal.md) page for details.
4. **Invoke from chat.** Open the test webapp, sign in, and send a message that mentions your handle, e.g. `use @YourHandle to ...`. Archer's `@handle` parser matches your row, the agent calls your endpoint, and the reply renders with a "via @YourHandle" attribution badge.

### Local smoke test before going live

The smoke test you want to run locally is simple: generate a one-shot Ed25519 keypair, serve the public key on a local well-known endpoint, sign a synthetic envelope, and POST it to your locally-running router. This catches canonical-JSON drift before you push.

The public [`archer-partner-examples`](https://github.com/Archer-Laboratories/archer-partner-examples) repo ships runnable Node and Python verifiers plus a signer you can wire into exactly this loop:

```bash
git clone https://github.com/Archer-Laboratories/archer-partner-examples
cd archer-partner-examples
# follow the repo README to install deps, sign a synthetic envelope with the
# bundled keypair, and POST it at your locally-running endpoint
```

Use the Node or Python verifier from that repo as a starter template for your own handler and follow the repo's `README.md` for the exact commands.

## Going live

Once your intent works end-to-end on `test`, promotion to production is a manual step today. Send your Archer contact:

1. The exact production `IntentDefinition` row payload (final `feeRate`, `payoutAddresses`, production `toolEndpoint`).
2. A pointer to your production endpoint (it must serve a successful signature verify against the **production** public key at `https://api.archerprotocol.com/.well-known/archer-public-key`).
3. Any verification artifacts you have (a security review, a domain you control, a GitHub org). These feed into your initial `verificationStatus` (`UNVERIFIED` -> `VERIFIED` -> `PREMIUM`), which is one of the inputs to your marketplace ranking score.

Archer engineering does the production insert during the dogfood window. Self-serve production publishing is on the near-term roadmap.

## Operational notes

* **Idempotency.** Always dedupe on `requestId`. Archer retries idempotent categories once on 5xx with the same `requestId`; without a dedupe layer you will double-execute on transient failures.
* **Clock skew.** Sync your server clock (NTP is fine). Envelopes timestamped more than 5 minutes off real time get rejected as replay attempts.
* **Public key caching.** Cache for an hour. On refresh failure, keep serving with the stale value and log a warning; rotation is rare and the endpoint sets explicit cache headers.
* **Logging.** Log at least `requestId`, `intentDefinitionId`, your route, upstream latency, and outcome. When something goes wrong, Archer's debug workflow asks you for the `requestId` first.
* **Errors that mean "ask the user."** If your endpoint needs more information from Alice (a missing parameter, ambiguous input), return `success: false` with a descriptive `error.message`. The LLM sees the message and either asks Alice or refines its tool call. Do not silently guess.
* **Errors that mean "I am unavailable."** Return `5xx` with a body shaped like the failure envelope. Idempotent categories will be retried once.

## What you do not have to do

* **Wallet management.** Archer holds the user-side wallet. You never see private keys, mnemonics, or anything else of that shape.
* **Chain RPCs.** Unless your intent itself talks to chain, Archer's agent layer handles all RPC work.
* **Billing reconciliation.** Archer's billing layer charges Alice for your reported `providerCostMicro`, the Archer TVR fee, and on-chain gas, all bundled into one ledger entry.
* **Attribution UI.** When the LLM picks your tool, Archer tags the reply with `Message.selectedIntentDefinitionId`; the test (and eventually production) webapp renders the "via @YourHandle" badge automatically.

## Related

* [Marketplace Intents](/archer.bot/intents/marketplace-intents.md) - the Alice-side picture of the marketplace.
* [Pricing & Fees](/archer.bot/for-builders/pricing.md) - how Alice is billed; explains how your rate composes with Archer's TVR fee.
* [Developer Portal](/archer.bot/for-builders/developer-portal.md) - API keys, approvals, and usage tracking for the Alice-side surface.
* [`archer-partner-examples`](https://github.com/Archer-Laboratories/archer-partner-examples) - the public reference verifiers this guide draws from.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://archer-bot.gitbook.io/archer.bot/for-builders/publishing-a-partner-intent.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
