---
name: ziggs-ziggspay
description: Agent-to-agent payments on Ziggs. Send money between wallets, delegate bounded spend to agents via payment grants, gate high-value transfers with HITL approvals, and fund wallets via Stripe. Works from any language through the HTTP API or via the ZiggsPayClient SDK class. Use this skill when an agent needs to pay, be paid, or when building a developer-facing product that rides the Ziggs payment rail.
metadata:
  author: ziggsAI
  version: "1.1"
  tier: "5"
  language: any
  requires: "@ziggs-ai/agent-sdk (optional) or plain HTTP"
  audience: developers
---

# ZiggsPay

Agent-to-agent payments. One rail for Ziggs-built agents and third-party developers — there is no "internal vs external" split.

## Mental model

- **Users (principals) are the only principal.** Some users are dev-enabled (`User.dev = { tier, kycStatus }`).
- **Wallets belong 1:1 to principals.** Agents do not own wallets.
- **Agents are delegates.** An agent spends from the owning principal's wallet under a **payment grant** the principal has issued to that agent.
- **Operator tokens** are the only credential. Payment-scoped operator tokens (`payments:spend`, etc.) are what you present in the `Authorization` header.

Every request authenticates with an operator token. When a launcher runs a specific agent, it additionally passes `X-Agent-Id` so the backend treats the call as agent-impersonated — which requires a `paymentGrantId` on money-moving endpoints.

This is the *same* principal/agent split documented in `identity-and-attribution.skill.md`, applied to money. The operator key proves hosting trust (who's running the agent); the payment grant proves delegation authority (whose wallet the agent may spend from). Compose with `sender.underAgreementId` on messages and the `creatorAgent` sidecar on agreements to record *which engagement* authorized each spend.

## Prerequisites

1. A Ziggs account with `isDeveloper: true` (sign up at `/ziggspay` or flip the flag on your existing user).
2. An operator token with payment scopes, minted at the portal or via `POST /operator-tokens`.
3. A funded wallet (dev faucet in sandbox, Stripe on-ramp in live).

## Base URL

```
https://api.ziggsai.com
```

## Scopes

Attach one or more to any operator token you mint:

| Scope              | Covers                                                                 |
|--------------------|------------------------------------------------------------------------|
| `payments:read`    | balance, history, policy, tokens, approvals (GET only)                 |
| `payments:spend`   | transfer, hold, release                                                |
| `payments:admin`   | set policy, issue / attenuate / revoke payment grants, decide approvals |
| `payments:onramp`  | Stripe intents, dev faucet, wallet fund                                |

`payments:admin` is excluded from the agent-issuable subset — an agent-impersonating key can spend, but cannot rewrite policy or mint payment grants on the user's behalf.

## Authentication

```
Authorization: Bearer <operator-token>
```

For agent-impersonated calls (launcher acting as a specific agent):
```
Authorization: Bearer <operator-token>
X-Agent-Id:    <agentId>
```

## Core endpoints

### Wallet

```
GET  /payments/wallet                 # balance + availableBalance
GET  /payments/wallets/resolve?userId=...  # lookup wallet by owner
GET  /payments/wallets/resolve?agentId=... # lookup wallet by owning agent
GET  /payments/history                # paginated transaction history
```

### Transfers

```
POST /payments/transfer
{
  "toWalletId":       "wal_...",
  "amount":           500,          // cents
  "idempotencyKey":   "unique-per-intent",
  "description":      "optional",
  "paymentGrantId": "required when X-Agent-Id is set"
}
```

Responses:
- `201 { status: "transferred", transaction }` on success.
- `202 { status: "approval_required", approval }` if the user's policy gates this amount.
- `403` if a payment grant is missing, invalid, doesn't belong to the caller's wallet, or doesn't match the impersonated agentId.

### Holds (escrow)

```
POST /payments/hold                    # reserve funds
POST /payments/release/:txId           # settle to toWalletId or refund
```

### payment grants

```
POST   /payments/grants                      # issue root token (requires payments:admin)
POST   /payments/grants/:grantId/attenuate   # mint a narrower child
DELETE /payments/grants/:grantId             # revoke (cascades to children)
GET    /payments/grants                      # list tokens for caller
```

Caveat vocabulary:

| Type                  | Value                      | Enforced at validate-time                     |
|-----------------------|----------------------------|-----------------------------------------------|
| `max_amount`          | cents                      | single-transfer cap                           |
| `daily_budget`        | cents                      | rolling 24h aggregate cap (per-token)         |
| `allowed_recipients`  | `["wal_...", ...]`         | transfer target must be in the list           |
| `expires_at`          | ISO timestamp              | absolute expiry                               |

### On-ramp + approvals

```
POST /payments/wallet/fund            # dev faucet (sandbox only)
POST /payments/onramp/intent          # Stripe PaymentIntent
POST /payments/onramp/mock-confirm/:intentId   # sandbox confirm
POST /payments/onramp/webhook         # Stripe webhook (signature-verified)

GET  /payments/approvals              # pending / all
POST /payments/approvals/:id/decide   # approve | reject
```

## SDK (JavaScript / TypeScript)

Install:
```bash
npm install @ziggs-ai/agent-sdk
```

### Direct-user client

```js
import { createZiggsPayClient } from "@ziggs-ai/agent-sdk";

const me = createZiggsPayClient({
  operatorKey: process.env.OPERATOR_KEY,
});

const wallet = await me.balance();

const result = await me.transfer({
  to: "wal_xyz",             // or a userId / agentId to auto-resolve
  amount: 500,
  description: "Invoice #001",
  idempotencyKey: "invoice-001",
});

if (result.status === "approval_required") {
  // Your own policy requires HITL. Decide from the portal or via:
  await me.decide(result.approvalId, "approve");
}
```

### Agent-impersonated client

```js
const agent = createZiggsPayClient({
  operatorKey:  process.env.OPERATOR_KEY,
  actAsAgentId: "agent_coffee_bot",
});

// The client fast-fails if you omit paymentGrantId for agent flows.
await agent.transfer({
  to: "wal_cafe",
  amount: 350,
  paymentGrantId: "pgr_...",
  idempotencyKey: "brew-2026-04-24-17:32",
});
```

### Agent tools (inside a Ziggs agent)

When your agent runs under `@ziggs-ai/agent-sdk`'s launcher, payment tools are pre-wired. Import `PAYMENT_TOOLS` and include them in `defineAgent(...)`:

```js
import { PAYMENT_TOOLS } from "@ziggs-ai/agent-sdk";

const config = defineAgent({
  agentId: "<YOUR_AGENT_ID>",
  tools: [...PAYMENT_TOOLS, ...yourOtherTools],
  // ...
});
```

Tools provided: `payment_balance`, `payment_transfer`, `payment_hold`, `payment_release`, `payment_budget`, `payment_resolve_wallet`. Each reads `ctx.operatorKey` + `ctx.agentId` from the launcher.

## Typical flow: user delegates bounded spend to an agent

```js
// 1. User mints a payment grant for their agent
const { token } = await user.issueToken({
  holderId: "agent_coffee_bot",
  caveats: [
    { type: "max_amount",         value: 500 },     // $5 max per tx
    { type: "daily_budget",       value: 2000 },    // $20/day
    { type: "allowed_recipients", value: ["wal_cafe_1", "wal_cafe_2"] },
    { type: "expires_at",         value: new Date(Date.now() + 86400000).toISOString() },
  ],
});

// 2. Agent uses it — bounded by every caveat. Backend enforces:
//    - token not revoked / expired
//    - token.rootWalletId === user's wallet
//    - token.holderId === X-Agent-Id
//    - each caveat passes
await agent.transfer({
  to: "wal_cafe_1",
  amount: 350,
  paymentGrantId: token.grantId,
  idempotencyKey: "brew-001",
});

// 3. User revokes at any time — cascades to attenuated children
await user.revokeToken(token.grantId);
```

## Curl examples

```bash
# Mint a payment-scoped operator token (logged-in user)
curl -X POST https://api.ziggsai.com/operator-tokens \
  -H "Authorization: Bearer <session-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "my-laptop",
    "scopes": ["payments:read", "payments:spend"]
  }'

# Direct transfer as a user
curl -X POST https://api.ziggsai.com/payments/transfer \
  -H "Authorization: Bearer <operator-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "toWalletId": "wal_xyz",
    "amount": 500,
    "idempotencyKey": "order-42"
  }'

# Agent-impersonated transfer (requires payment grant)
curl -X POST https://api.ziggsai.com/payments/transfer \
  -H "Authorization: Bearer <operator-token>" \
  -H "X-Agent-Id: agent_coffee_bot" \
  -H "Content-Type: application/json" \
  -d '{
    "toWalletId":         "wal_cafe",
    "amount":             350,
    "idempotencyKey":     "brew-001",
    "paymentGrantId":  "pgr_..."
  }'
```

## Invariants you can rely on

- Balance is always derived from the ledger (`SUM(credits) − SUM(debits)`). No mutable balance column exists — drift is impossible by construction.
- Money-moving paths run inside a MongoDB transaction; concurrent transfers cannot double-spend.
- Every transfer attributes its source via `metadata.agentId` + `metadata.paymentGrantId` when impersonation is in play.
- Idempotency keys are required on `/transfer`, `/hold`, `/release`, and `/wallet/fund`. Retries are safe.

## Common errors

| HTTP | Cause                                                                                        |
|------|----------------------------------------------------------------------------------------------|
| 401  | Missing / invalid operator token, or the key was revoked                                     |
| 403  | Missing scope, missing `paymentGrantId` on an agent-impersonated call, or caveat failure  |
| 400  | Negative / zero amount, same-wallet transfer, missing `idempotencyKey`, expired token        |
| 404  | Wallet or hold not found                                                                     |
| 202  | Transfer gated on human approval — inspect the returned `approval` and decide later          |

## Operational notes

- `CAPABILITY_ROOT_SIGNING_KEY` must be set in the backend env in production; tokens are Ed25519-signed with this 32-byte root seed (hex or base64). Verifiers need only the derived public key, so the seed can live in KMS later without API changes.
- Mongo must be a replica set (transactions are required on money paths).

NEVER share your operator token. Store it in a secret manager. Rotate via the portal or `POST /operator-tokens/:keyId/revoke` + re-mint.
