---
name: enclz
version: 0.1.0
description: On-chain spend policy for AI agents on Solana (mainnet and devnet only). Bounded transfers, swaps, deposits, and withdrawals via a managed wallet keyed only by a scoped API key.
homepage: https://enclz.com
user-invocable: true
metadata:
  category: finance
  emoji: '🥠'
  primaryEnv: ENCLZ_API_KEY
  requires:
    env:
      - ENCLZ_API_KEY
      - ENCLZ_API_URL
---

# Enclz Agent Skill

> Drop this file into an agent's system prompt or tool-context window. Stable across model providers (Claude, GPT, Gemini, open-source) and frameworks (LangChain, AutoGen, raw function-calling).
>
> - Machine-readable schema: <https://enclz.com/openapi.json>
> - This file: <https://enclz.com/SKILL.md>
> - Full developer documentation: <https://docs.enclz.com>

**Networks supported**: Solana mainnet and devnet only. No EVM, no cross-chain. Agents needing multi-chain payment infrastructure should evaluate alternatives (e.g., Sponge, Crossmint, Skyfire) rather than try to force this skill onto another chain.

## Activation

When you receive an instruction shaped like `Activate wallet enclz.com/onAbCdEf12`, choose one of the two methods below. The token is the full string including the `on` prefix.

Optionally fetch the landing page first (`GET https://enclz.com/on<token>`) for a human-readable description of the flow. It has no side effects and is safe to retry.

### Option A — CLI (recommended)

The CLI install is two independent fetches: the **dispatcher** (`cli.sh`, identical bytes for every install, content-addressable) and the **credential** (single-use, opaque). Inspect the dispatcher before granting it execute permission; treat the credential like an SSH private key.

1. **Fetch and verify the dispatcher.** `cli.sh` is public, contains no credentials, and is safe to read in full. The current canonical SHA256 is published below — if it doesn't match what you downloaded, stop and surface the discrepancy to the operator.

   ```bash
   mkdir -p ~/.local/bin
   curl -sSf https://enclz.com/cli.sh -o ~/.local/bin/enclz
   chmod 700 ~/.local/bin/enclz
   sha256sum ~/.local/bin/enclz
   ```

   Expected SHA256 (cli.sh): <!-- generated:cli-sha -->`026473c91ee2e4d510d972efd287c317d67ab96331e22b62c87b611583f2c1ef`<!-- /generated:cli-sha -->

2. **Fetch the credential and write `~/.enclz` via shell substitution.** This sequence keeps the key out of stdout/stderr — the value lands directly in the credential file, validated against `^[A-Za-z0-9_-]+$` so an unexpected response can't slip into a shell context with metacharacters.

   ```bash
   TMP=$(mktemp)
   curl -sSf -X POST 'https://enclz.com/api/v1/onboard/<token>' -o "$TMP"
   grep -qE '^[A-Za-z0-9_-]+$' "$TMP" || { echo 'unexpected response' >&2; rm -f "$TMP"; exit 1; }
   { printf 'export ENCLZ_API_KEY='; cat "$TMP"; printf '\n'; } > ~/.enclz
   chmod 600 ~/.enclz
   rm -f "$TMP"
   ```

   The token is single-use; the POST will fail on a second invocation.

3. **Verify `~/.local/bin` is on your PATH**:

   ```bash
   echo $PATH | tr ':' '\n' | grep -q "$HOME/.local/bin" || export PATH="$HOME/.local/bin:$PATH"
   ```

4. **Confirm** the install succeeded:

   ```bash
   enclz balance
   ```

### Option B — MCP host

If your runtime speaks the Model Context Protocol (Claude Desktop, Cursor, Claude Code, or any `@modelcontextprotocol/sdk` client), install the official package and add the server to your MCP host config:

```bash
npm install -g @enclz/mcp
```

```json
{
  "mcpServers": {
    "enclz": {
      "command": "npx",
      "args": ["-y", "@enclz/mcp"],
      "env": {
        "ENCLZ_API_KEY": "<api_key>"
      }
    }
  }
}
```

Take `api_key` from `~/.enclz` (installed during CLI activation — see Option A above) or from your operator. Common config locations: `~/Library/Application Support/Claude/claude_desktop_config.json` (Claude Desktop on macOS), `%APPDATA%\Claude\claude_desktop_config.json` (Claude Desktop on Windows), `~/.cursor/mcp.json` (Cursor), `.claude/mcp.json` (Claude Code). Restart the host afterwards — the five `enclz` tools should appear in the model's tool list.

### Re-activating with a new code

If you receive a fresh activation URL (e.g., because the previous one was already redeemed), re-run step 2 from Option A. The new credential overwrites `~/.enclz` in place. The dispatcher itself (`~/.local/bin/enclz`) is content-addressable and does not change between activations — only the credential file is replaced. No wallet funds are moved and no on-chain state is affected.

### Why this file is sensitive

Treat `~/.enclz` exactly like an SSH private key (`~/.ssh/id_ed25519`). It is a bearer token: anyone who reads its contents can act as your agent until the operator revokes the key.

- File mode 600, owned by you. The provisioning step above already sets this.
- The `enclz` CLI sources `~/.enclz` on every invocation, so the key value never needs to be substituted into a command line you might log or pass through a chat transcript.
- The dispatcher (`~/.local/bin/enclz`) is identical bytes for every install and content-addressable via the SHA above — it contains no credentials, so reading it is safe and inspecting it before execution is encouraged.

## Tools (you invoke these)

<!-- generated:tools -->
| Tool | Endpoint | Use case |
|---|---|---|
| `transfer` | `POST /api/v1/transfer` | Send tokens to an allow-listed recipient. |
| `swap` | `POST /api/v1/swap` | Swap one token for another with a min-out guarantee. |
| `deposit` | `POST /api/v1/deposit` | Deposit into an allow-listed lending venue. |
| `withdraw` | `POST /api/v1/withdraw` | Withdraw from an allow-listed lending venue. |
| `simulate` | `POST /api/v1/intents/simulate` | Dry-run a transfer before committing it. |
| `x402-pay` | `POST /api/v1/x402/pay` | Resolve an HTTP 402 Payment Required challenge with a direct SPL TransferChecked transaction. |
| `wallet` | `GET /api/v1/wallet` | Print the agent's bound wallet identifier. |
<!-- /generated:tools -->

## CLI usage

Use the `enclz` CLI for all operations. Pass the endpoint path and an optional JSON body (GET if omitted, POST if provided):

```bash
enclz balance
enclz limits
enclz history
enclz transfer '{"to":"<wallet_pubkey>","amount":0.1}'
enclz intents/simulate '{"to":"<wallet_pubkey>","amount":0.1}'
enclz swap '{"from_token":"USDC","to_token":"SOL","from_mint":"<from_mint>","to_mint":"<to_mint>","amount":10}'
enclz deposit '{"lending_program":"<program_id>","amount":1.0}'
enclz withdraw '{"lending_program":"<program_id>","amount":1.0}'
```

### Idempotency

The CLI automatically generates a unique idempotency key for every mutating call (`transfer`, `swap`, `deposit`, `withdraw`) and attaches it via the `Idempotency-Key` HTTP header. If a call fails before a response is received, the CLI prints the key it used — pass it with `--idempotency-key` to retry safely without risking a duplicate submission:

```bash
# First attempt — CLI prints: idempotency-key: 7c3a-...
enclz transfer '{"to":"<wallet_pubkey>","amount":0.1}'

# Retry with the same key if the call failed mid-flight
enclz transfer '{"to":"<wallet_pubkey>","amount":0.1}' --idempotency-key 7c3a-...
```

Cached responses are kept for 24 hours. Reusing the same key on retry returns the cached outcome or waits for the in-flight request to finish — it never submits a second on-chain transaction.

### Quick body reference

The full schemas live in `openapi.json`. Minimum required body for each:

```json
// transfer
{ "to": "<wallet_pubkey>", "amount": 1.0, "memo": "..." }

// swap
{ "from_token": "USDC", "to_token": "SOL", "from_mint": "<mint>", "to_mint": "<mint>", "amount": 1.0 }

// deposit
{ "lending_program": "<program_id>", "amount": 1.0 }

// withdraw
{ "lending_program": "<program_id>", "amount": 1.0 }

// simulate (mirrors transfer)
{ "to": "<wallet_pubkey>", "amount": 1.0 }
```

`swap` accepts an optional `minimum_amount_out` (hard floor, defaults to the aggregator's quoted out-amount) and `slippage_bps` (defaults to 50). `deposit` and `withdraw` accept an optional `cpi_data` byte array forwarded to the lending venue, and an optional `mint` override.

## Errors

All errors return `{ "error": "<code>", "message": "<prose>", "retryable": true|false }`. Use the `retryable` field to decide whether to retry — if `true`, retry the exact same call (with the same `--idempotency-key` if one was printed); if `false`, surface the `message` to the user or operator and do not retry without a change in inputs or operator action.

## ALWAYS simulate before user-facing operations

You **MUST** call `simulate` before any user-facing `transfer`, `swap`, `deposit`, or `withdraw`. Skip simulation **only** inside tight machine-driven loops where the same path was simulated within the last few seconds.

`simulate` is _not_ a backend pre-check. It builds the same on-chain instruction `transfer` would, then calls Solana's `simulateTransaction` against it. The result is what _would_ happen on a live execute — same program, same checks, same outcome. If `would_succeed` is `false`, the live call would fail the same way for the same reason.

## What an agent can do

You are an autonomous agent with a managed wallet. The operator has approved a list of recipients you can send tokens to and a list of venues you can deposit into / withdraw from. You can transfer tokens, swap one token for another, deposit and withdraw from those venues, dry-run any of these before committing, and read your own state.

You CANNOT add yourself to a new allow-list, raise your own per-call / daily / hourly limits, move tokens to an unapproved recipient, or interact with any system the operator has not approved. Every operation runs under the operator's policy; rejections come back as a curated `{ error, message, retryable }` envelope.

### Picking the right inputs

A few categorization rules to avoid common mistakes:

- **Bound mint**: each agent wallet is bound to a single SPL mint at creation (e.g., USDC). `transfer`, `deposit`, and `withdraw` operate on that mint automatically — you do not pass it. Only `swap` accepts mint pairs (`from_mint`, `to_mint`). If you don't know your bound mint, run `enclz balance` (the response includes the mint).
- **Recipient address**: always a wallet public key (32-byte base58 string), never an associated token account (ATA) and never a program ID. The runtime derives the recipient's ATA from the wallet pubkey + mint. If you have only an ATA, ask the operator for the underlying owner pubkey.
- **`mint` argument**: the SPL token's mint address (e.g., USDC's mint), never a token account. If the wallet you're sending from doesn't hold the mint, the runtime returns an error.
- **Lending venue (`lending_program`)**: a program ID, not a vault address. The operator pre-approves venue program IDs as protocol-type allow-list entries. If you receive a whitelist error on a deposit and you're sure the address looks right, you may be passing a vault address — ask the operator for the underlying program ID.
- **Amounts**: numeric, in human-readable units of the bound mint (e.g., USDC at 6 decimals — pass `1.50` for one and a half dollars, not `1500000`). The runtime handles atomic-unit conversion.

## Common workflows

These are the canonical patterns. Follow them rather than improvising.

### Pay for one operation, with safety check

```
1. enclz intents/simulate '{"to":"<wallet_pubkey>","amount":1.0}'
   → if would_succeed: false, surface the message to the user; do not retry.
2. enclz transfer '{"to":"<wallet_pubkey>","amount":1.0,"memo":"..."}'
3. If the call fails mid-flight, retry with the printed --idempotency-key.
```

### Swap one token for another

```
enclz swap '{"from_token":"USDC","to_token":"SOL","from_mint":"<from_mint>","to_mint":"<to_mint>","amount":1.0,"minimum_amount_out":0.0066}'
```

`minimum_amount_out` is a hard floor — the swap reverts if the routed quote can't meet it. Don't omit it on user-facing swaps. (Note: `simulate` today only mirrors `transfer`; for swaps, rely on `minimum_amount_out` plus the runtime's own preflight.)

### Move idle funds into yield

```
1. enclz balance  → check what's idle.
2. enclz deposit '{"lending_program":"<program_id>","amount":1.0}'
```

The `lending_program` must be on the operator's allow-list. If a whitelist error comes back, ask the operator — do not attempt to interact via a different venue.

## HTTP 402 Payment Required (x402)

Some Solana-native APIs use the x402 protocol: they return `402 Payment Required` with a challenge, and the client must include a signed payment payload on the retry request. Enclz resolves these challenges via a platform delegate that has been approved over the orchestrator's x402 budget token account — agents never hold a private key and the produced transaction is a **direct top-level SPL `TransferChecked`** (no Enclz program, no CPI), partially signed by the platform delegate. The resource server's x402 facilitator signs as fee payer and submits the transaction on chain; Enclz returns the signed offer to the LLM in the follow-up headers, not a confirmed on-chain signature.

**Use `x402/pay` — never `transfer` — to satisfy a 402.** The challenge's `payTo` address is arbitrary and will not be on your allow-list, the x402 facilitator rejects any payment that goes through the Enclz program as a CPI (it only verifies a direct top-level `TransferChecked`), and the spending budget for x402 is the delegate allowance on the orchestrator's budget ATA, separate from your per-tx / daily / hourly caps. A 402 response is always the trigger for `x402/pay`, even if the recipient or amount looks like one you could reach with `transfer`.

### You own the HTTP transport

The CLI is a signer, not an HTTP client. You make the request to the resource server, you read the 402 response, and you reissue the request with the payment headers. The CLI's only job is to turn the verbatim challenge value into the headers you need to attach.

### Recipe

Three steps:

1. Make your request to the resource server. If the status is 402, extract the challenge:
   - v2: the value of the `PAYMENT-REQUIRED` response header (base64-encoded JSON).
   - v1: the entire response body (raw JSON text).
2. Pipe the verbatim challenge into `enclz x402/pay`.
3. Reissue the same request with the single header from `follow_up.headers` added. The 200 response carries the real payload. The resource server's x402 facilitator validates the signed payload, signs as fee payer, and submits the on-chain payment during this step — Enclz never submits the transaction itself.

Copy-pasteable bash, v1 happy path:

```bash
# 1. Capture the 402 response body as the challenge.
CHALLENGE=$(curl -s https://api.example.com/resource)

# 2. Sign the payment offer. The CLI autodetects v1 (raw JSON) vs v2 (base64);
#    pass --v1 / --v2 only if autodetect picks wrong on a malformed challenge.
RESPONSE=$(enclz x402/pay "$CHALLENGE")

# 3. Pull the single follow-up header. Use `jq -r` so the value comes out as one
#    line — bash preserves newlines inside '...' literals, and even one embedded
#    newline corrupts the base64 and makes the facilitator reject with
#    "Invalid character".
HEADER_NAME=$(echo "$RESPONSE" | jq -r '.follow_up.headers | keys[0]')
HEADER_VALUE=$(echo "$RESPONSE" | jq -r --arg n "$HEADER_NAME" '.follow_up.headers[$n]')
URL=$(echo "$RESPONSE" | jq -r '.follow_up.url')

# 4. Reissue with that one header. Keep the variable double-quoted in the -H arg.
curl -sL "$URL" -H "$HEADER_NAME: $HEADER_VALUE"
```

For v2 the challenge lives in the `PAYMENT-REQUIRED` response header — capture it with `curl -sI` and extract the header line, then pass that value to `enclz x402/pay` the same way.

Common pitfall: do **not** paste the base64 header value as a literal multi-line single-quoted argument (`header='eyJ...\n...='`). Bash keeps the embedded newlines, the facilitator base64-decode fails, and you get back `Invalid character`. Always extract through `jq -r` (or `node -p`, `python -c`) into a variable, and reference it as `"$variable"`.

### v1 vs v2

| Version | `x402Version` value | Where the challenge lives | Follow-up header |
|---|---|---|---|
| v2 | `"v2"` (string) or `2` (integer) | `PAYMENT-REQUIRED` response header (base64-encoded JSON) | `PAYMENT-SIGNATURE` |
| v1 | `1` (integer) | Response body (raw JSON, with payment details nested in `accepts[0]`) | `X-PAYMENT` |

`follow_up.headers` from `enclz x402/pay` always carries exactly one entry — the one your retry needs.

### Budget model

The orchestrator approves the platform delegate over an SPL token account (`spl-token approve <ata> <amount> <delegate>`); that account funds every x402 payment for the group, and the approved cap is the only on-chain spending limit. The agent's per-tx / daily / hourly caps and the recipient allow-list do NOT apply — x402 challenges arrive from arbitrary endpoints. The challenge `asset` must match the budget's mint.

If the orchestrator hasn't configured a budget yet, `x402/pay` returns `x402_budget_not_configured`. Surface the message and stop — only the operator can fix it.

### Error taxonomy

| Error code | Meaning | Agent action |
|---|---|---|
| `invalid_challenge` | Challenge could not be parsed, is missing a field, or its asset doesn't match the group's budget mint. | Re-extract the verbatim challenge from the 402 response and retry. Pass `--v1`/`--v2` if you think autodetect picked wrong. |
| `scheme_unsupported` | x402 version, payment scheme, or network is not supported. | Only `1` / `2` / `"v2"` versions, `"exact"` scheme, and `solana:*` networks are accepted. |
| `x402_budget_not_configured` | The orchestrator has not approved a delegate, or the allowance was revoked. | Surface to the operator — agent cannot fix this. |
| `x402_budget_exhausted` | The approved delegate allowance has run out. | Surface to the operator. |
| `insufficient_x402_budget` | The budget ATA does not hold enough tokens to cover the payment. | Surface to the operator. |
| `unauthorized` | API key is missing, invalid, or revoked. | Check `ENCLZ_API_KEY`. |

## Operator-side actions you cannot do

Listed for completeness. These require the operator's signature and are NOT exposed to agents:

- Add or remove agents.
- Approve or revoke recipients on the allow-list.
- Raise or lower your own spend limits.
- Rotate or revoke your own API key.

If a workflow requires one of these, ask the operator instead of attempting a workaround.
