---
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/AbCdEf12`:

1. **Fetch the landing page** (`GET https://enclz.com/<token>`) — returns Markdown describing the next two steps. No side effects; safe to retry.
2. **Download credentials directly to disk** with the command the landing page provides. It POSTs `https://enclz.com/api/v1/onboard/<token>` and pipes the response straight to `~/.config/enclz/credentials.env`. The token is single-use; this step redeems it.
3. **Inject credentials via env vars** on every subsequent call:

   ```bash
   source ~/.config/enclz/credentials.env
   curl -sSf -H "Authorization: Bearer ${ENCLZ_API_KEY}" \
     "${ENCLZ_API_URL}/api/v1/balance"
   ```

**Hard rules.** Never read the credentials file's contents into your context. Never echo, cat, log, or paste the API key. The shell expands `${ENCLZ_API_KEY}` at request time so the secret never enters the model's transcript. If you find yourself about to read the file or print the key, stop — you don't need its value, you need the env var injected into the curl process.

If you lose the file, ask the operator for a fresh activation URL — any pending invitation is invalidated when a new one is minted.

## 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. |
<!-- /generated:tools -->

### Quick body reference

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

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

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

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

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

// simulate (mirrors transfer — there is no `kind` discriminator today)
{ "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. `transfer` accepts an optional `mint` override and `task_id`.

### Quick HTTP examples

For non-MCP runtimes, here is one canonical invocation per mutating tool. Replace `${ENCLZ_API_URL}` and `${ENCLZ_API_KEY}` from your activation manifest, generate a fresh UUID for `<uuid>`, and substitute the placeholder addresses with values from your operator's allow-list.

```bash
# Send tokens to an allow-listed recipient
curl -sSf -X POST "${ENCLZ_API_URL}/api/v1/transfer" \
  -H "Authorization: Bearer ${ENCLZ_API_KEY}" \
  -H "X-Enclz-Version: 0.1.0" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "<wallet_pubkey>",
    "amount": 1.0,
    "memo": "rpc-call-#a8f2",
    "idempotency_key": "<uuid>"
  }'

# Swap one token for another via Jupiter
curl -sSf -X POST "${ENCLZ_API_URL}/api/v1/swap" \
  -H "Authorization: Bearer ${ENCLZ_API_KEY}" \
  -H "X-Enclz-Version: 0.1.0" \
  -H "Content-Type: application/json" \
  -d '{
    "from_token": "USDC",
    "to_token": "SOL",
    "from_mint": "<from_mint>",
    "to_mint": "<to_mint>",
    "amount": 1.0,
    "minimum_amount_out": 0.0066,
    "idempotency_key": "<uuid>"
  }'

# Deposit to an allow-listed lending venue
curl -sSf -X POST "${ENCLZ_API_URL}/api/v1/deposit" \
  -H "Authorization: Bearer ${ENCLZ_API_KEY}" \
  -H "X-Enclz-Version: 0.1.0" \
  -H "Content-Type: application/json" \
  -d '{
    "lending_program": "<program_id>",
    "amount": 1.0,
    "idempotency_key": "<uuid>"
  }'

# Withdraw from an allow-listed lending venue (same shape as deposit)
curl -sSf -X POST "${ENCLZ_API_URL}/api/v1/withdraw" \
  -H "Authorization: Bearer ${ENCLZ_API_KEY}" \
  -H "X-Enclz-Version: 0.1.0" \
  -H "Content-Type: application/json" \
  -d '{
    "lending_program": "<program_id>",
    "amount": 1.0,
    "idempotency_key": "<uuid>"
  }'

# Dry-run a transfer before committing it
curl -sSf -X POST "${ENCLZ_API_URL}/api/v1/intents/simulate" \
  -H "Authorization: Bearer ${ENCLZ_API_KEY}" \
  -H "X-Enclz-Version: 0.1.0" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "<wallet_pubkey>",
    "amount": 1.0
  }'
```

## Resources (auto-attached to context)

The MCP host fetches these every turn so you don't need to call them as tools. If you're using the REST API directly, hit the same endpoints with `GET`.

<!-- generated:resources -->
| Resource URI | Endpoint | Use case |
|---|---|---|
| `enclz://balance` | `GET /api/v1/balance` | Current balances and remaining headroom. |
| `enclz://limits` | `GET /api/v1/limits` | Active spend limits and counters. |
| `enclz://history` | `GET /api/v1/history` | Recent confirmed-intent activity. |
<!-- /generated:resources -->

## Errors

All errors return `{ "error": <code>, "message": <prose> }`. The codes below are the complete set the runtime emits — they are deliberately business-level; you should never see internal stack traces, low-level codes, or vendor-specific terminology.

<!-- generated:errors -->
| Error code | Remediation |
|---|---|
| `whitelist_violation` | Recipient is not on the operator-managed allow-list. Ask the operator to approve the address. |
| `whitelist_expired` | Allow-list entry has reached its TTL. Operator must renew. |
| `whitelist_amount_exhausted` | Recipient has reached its approved spend cap. Operator must extend. |
| `daily_limit_exceeded` | Today's spend cap reached. Wait for the daily reset (UTC) or ask the operator to raise the cap. |
| `per_tx_limit_exceeded` | Single-call amount above the per-call limit. Split into smaller operations. |
| `hourly_cap_exceeded` | Hourly call cap reached. Wait for the next hour boundary. |
| `unauthorized` | Authentication failed. Check the API key. |
| `invalid_amount` | Amount is zero or otherwise invalid. Validate inputs before retry. |
| `invalid_mint` | Token not allowed by the operator. Use an approved token. |
| `invalid_token_account` | Token account is malformed or owned by the wrong key. Operator should investigate. |
| `invalid_fee_account` | Protocol-fee account mismatch. Operator should investigate. |
| `invalid_invitation_code` | Invitation code expired, used, or unknown. Ask the operator to mint a new one. |
| `idempotency_in_progress` | A prior request with the same idempotency key is still in flight. Retry with the same key. |
| `jupiter_unavailable` | The swap routing service is temporarily unavailable. Retry with backoff. |
| `token_not_in_registry` | Token isn't registered for this group. Ask the operator to add it before retrying. |
| `recipient_invalid` | Recipient address is malformed, identical to your own wallet, or otherwise rejected. Use a different recipient. |
| `account_not_initialized` | A token account required for this operation has not been initialized yet. Ask the operator to fund it. |
| `insufficient_balance` | The wallet does not hold enough of this token to cover the operation. Reduce the amount or wait for a top-up. |
| `validation_error` | Request body or query parameters did not pass validation. Inspect `details[]` on the response for the failing fields. |
| `internal_error` | The request could not be processed. Retry shortly; if the problem persists contact the operator. |
<!-- /generated:errors -->

### Error retryability

When you receive an error, choose your retry strategy by category:

**Retryable now** — same call, same idempotency key:

- `idempotency_in_progress` (a prior call with this key is still running)
- `jupiter_unavailable` (swap routing temporarily down)
- `internal_error` (transient backend issue)

**Retryable after a wait, no input change** — the cap will refresh:

- `daily_limit_exceeded` (wait for the rolling 24h window to advance)
- `hourly_cap_exceeded` (wait for the rolling 60-min window to advance)

**Inspect input, do not retry blindly** — bug in the call:

- `invalid_amount`
- `invalid_mint`
- `invalid_token_account`
- `invalid_fee_account`
- `recipient_invalid`
- `validation_error`
- `per_tx_limit_exceeded` (split into smaller calls and retry; do not raise the call's amount)

**Needs the operator** — surface to the user/orchestrator, do not retry:

- `whitelist_violation` (recipient not approved)
- `whitelist_expired` (allow-list TTL passed)
- `whitelist_amount_exhausted` (recipient budget consumed)
- `unauthorized` (API key invalid)
- `invalid_invitation_code` (single-use code already redeemed)
- `account_not_initialized` (token account not funded)
- `token_not_in_registry` (mint not registered for this group)
- `agent_inactive` (operator paused or revoked the agent)
- `insufficient_balance` (top-up needed)

## 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 }` 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, read `enclz://balance` (in MCP) or `GET /api/v1/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 `invalid_mint`.
- **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 `whitelist_violation` 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.

## Idempotency

Every mutating call (`transfer`, `swap`, `deposit`, `withdraw`) accepts an `idempotency_key` field on the body and mirrors it via the `Idempotency-Key` HTTP header. **Always reuse the same key on retry.** Generating a new key on retry submits a duplicate operation. Cached responses are kept for 24 hours.

If you receive `idempotency_in_progress`, a prior request with the same key is still running — retry the call (the cached response will be returned once the original finishes).

**Retry pattern:**

```
attempt 1: POST /api/v1/transfer  Idempotency-Key: 7c3a-...  → 503 jupiter_unavailable
attempt 2: POST /api/v1/transfer  Idempotency-Key: 7c3a-...  → 200 confirmed
                                  ^ same key — gets the cached attempt-1 outcome OR a fresh
                                    completion if attempt-1 never made it past the dispatcher.
```

If you generate a new key on retry instead, you submit a duplicate operation. The on-chain enforcement still applies (so it can't double-spend past your daily cap), but you may waste a per-tx limit slot.

## 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.

## Authentication

Send `Authorization: Bearer <api_key>` on every call, where `<api_key>` is the value from your activation manifest (see [Activation](#activation)). If you no longer have the key, ask your operator to mint a fresh activation URL.

Also send `X-Enclz-Version: 0.1.0` on every request — this lets the runtime evolve safely without silently breaking older agents. If a response includes an `X-Enclz-Version-Mismatch` header, the runtime is on a different version than this skill: complete the current call, then re-fetch the skill (`https://enclz.com/SKILL.md`) before the next call to avoid drift. The runtime treats version mismatch as a soft warning today and will hard-reject in a future release.

## Optional: MCP host integration

If your runtime speaks the Model Context Protocol (Claude Desktop, Cursor, Claude Code, or any `@modelcontextprotocol/sdk` client), the official `@enclz/mcp` package exposes the five mutating operations above as native MCP tools and the three reads as auto-attached MCP resources, so you don't have to make raw HTTP calls.

Take `api_key` and `api_url` from your activation manifest and add the server to your MCP host config:

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

Common 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 and three `enclz://` resources should appear in the model's tool list.

## Common workflows

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

### Pay for one operation, with safety check

```
1. simulate({ to, amount })
   → if would_succeed: false, surface the error to the user; do not retry.
2. transfer({ to, amount, memo, idempotency_key: <new uuid> })
3. On idempotency_in_progress: retry with the same key (exponential backoff).
```

### Swap one token for another

```
1. swap({ from_token, to_token, from_mint, to_mint, amount, minimum_amount_out, idempotency_key: <new uuid> })
   → if the response surfaces a slippage-related error, raise slippage_bps or split the order.
```

`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. balance() → check what's idle.
2. deposit({ lending_program, amount, idempotency_key })
```

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

### React to an exhausted recipient cap

When you receive `whitelist_amount_exhausted`, the operator's pre-approved budget for that recipient is fully consumed. Do not retry. Surface the error to the user/orchestrator and ask whether the operator should top up the recipient's allowance.

### React to an expired recipient

`whitelist_expired` means the operator's TTL on that recipient has passed. Same response: surface it, request renewal. Do not retry the same operation.

## 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.
