# YAML Specification

> Declare a subscription endpoint in your provider spec.

A subscription endpoint adds a `subscription:` block in place of `metering:`. The gateway reads the block, looks up the on-chain `Plan` you've published, and emits a 402 with `intent="subscription"` for any caller that hasn't activated yet.

For the full provider-spec shape (top-level fields, routing, operator, endpoints), see [YAML Specification](/docs/building-with-pay/yaml-specification). This page covers only the `subscription:` block.

## Agent summary

- `subscription:` and `metering:` are mutually exclusive on the same endpoint.
- `period` accepts a count plus `d` / `day` / `days` or `w` / `week` / `weeks`. `month` is rejected.
- The mapped per-period interval must fall in `[1h, 8760h]`. Up to one year per period.
- `plan_id` is written back into the file by `pay --sandbox server start` on first launch.
- Operator-level `recipient` and `puller` defaults apply unless overridden per endpoint.

## Minimal example

The smallest complete provider spec with one subscription endpoint — $9.99 USDC every 30 days.

<DownloadSpec file="monthly.yml">

```yaml
endpoints:
  - method: GET
    path: 'api/v1/pro/feed'
    resource: 'pro-feed'
    description: 'Pro feed — monthly subscription, 30-day billing period.'
    subscription:
      period: '30d'
      price_usd: 9.99
      currency: USDC
```

</DownloadSpec>

Serve it:

```sh
pay --sandbox server start monthly.yml
```

The first launch prompts you to publish the on-chain `Plan` PDA — sandbox covers the rent. After publishing, the gateway binds to `127.0.0.1:1402` and writes the resulting plan address back into the YAML so subsequent launches reuse the same PDA.

Consume it from another terminal:

```sh
# First call — activates the subscription, $9.99 USDC charge settles.
pay --sandbox curl http://127.0.0.1:1402/api/v1/pro/feed

# Same call within 30 days — no payment prompt, just the response.
pay --sandbox curl http://127.0.0.1:1402/api/v1/pro/feed
```

## Field reference

| Field        | Required | Description                                                                                                           |
| ------------ | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `period`     | Yes      | Billing period as count + unit. Accepts `d`/`day`/`days` or `w`/`week`/`weeks`. Example: `30d`, `2w`.                 |
| `price_usd`  | Yes      | Per-period charge in USD. Converted to mint base units at challenge time.                                             |
| `currency`   | Yes      | Mint symbol (`USDC`, `USDT`, …) or base58 mint address. Resolved against the operator's `currencies` map.             |
| `plan_id`    | No\*     | Base58 of the on-chain `Plan` PDA. Written back by `pay --sandbox server start`. Required at runtime.                 |
| `expires_at` | No       | RFC 3339 timestamp. After this point the server stops renewing and returns fresh 402 challenges. HTTP-layer cap only. |
| `puller`     | No       | Base58 of the wallet authorized to submit `transfer_subscription`. Defaults to the operator's pubkey.                 |
| `recipient`  | No       | Base58 of the wallet that receives the charge. Defaults to the operator-level recipient.                              |

\* Required for the endpoint to serve traffic. `pay --sandbox server start` will publish the plan interactively on first launch and write the address back.

## Period unit constraints

The on-chain program uses fixed elapsed-time periods, so `period` cannot be `month` (months are not a fixed number of hours). The parser maps:

- `Nd` / `N day` / `N days` → `N * 24` hours
- `Nw` / `N week` / `N weeks` → `N * 168` hours

A period of `0h` or more than `8760h` (one year) is rejected at spec-load time.

| Spec value | Mapped hours | OK?                 |
| ---------- | ------------ | ------------------- |
| `1d`       | 24           | ✓                   |
| `30d`      | 720          | ✓                   |
| `2w`       | 336          | ✓                   |
| `52w`      | 8736         | ✓                   |
| `1y`       | n/a          | ✗ unsupported unit  |
| `1m`       | n/a          | ✗ unsupported unit  |
| `400d`     | 9600         | ✗ exceeds 8760h cap |

## Publishing the plan

The YAML block alone is not enough — the gateway refuses to serve traffic until a corresponding `Plan` PDA exists on-chain. `pay --sandbox server start` handles this for you on first launch:

```sh
pay --sandbox server start provider.yml
```

For each subscription endpoint without a `plan_id`, the command derives the deterministic Plan PDA, RPC-checks whether it exists, and prompts:

```
? Publish create_plan on-chain now? (costs ~0.001 SOL in rent + fees) (y/n) › no
```

Answer yes and the gateway broadcasts `create_plan`, then writes the resulting `plan_id`, `plan_id_numeric`, `plan_bump`, and `plan_created_at` back into the YAML. Commit the updated file — the plan address is now part of your provider contract.

For a non-interactive derive-only workflow (e.g. CI), use `pay server plans publish --dry-run --owner <pubkey>`.

## Defaults from operator config

Operator-level config bleeds into every endpoint unless overridden:

```yaml
operator:
  currencies:
    usd: ['USDC']
  network: 'mainnet'
  recipient: '9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin'
  puller: '5fKb5cF22cFybZB1H4hLDydFhwoQy9JzKzRWaSbMkB6h'
```

If you omit `subscription.recipient` and `subscription.puller`, the endpoint inherits these. Override per-endpoint only when a specific subscription must route to a different wallet or be pulled by a different operator key.

## Mutually exclusive with metering

A single endpoint declares one of `metering:` or `subscription:`, never both. Use a second endpoint if you need both a per-call price and a subscription tier on the same resource:

```yaml
endpoints:
  - method: GET
    path: 'api/v1/quote/{symbol}'
    metering:
      dimensions:
        - direction: usage
          unit: requests
          tiers:
            - price_usd: 0.01

  - method: GET
    path: 'api/v1/pro/quote/{symbol}'
    subscription:
      period: '30d'
      price_usd: 9.99
      currency: USDC
```

## Next

- [Examples](/docs/building-with-pay/subscriptions/examples) — monthly, weekly, annual, multi-tier — six runnable specs you can download and serve.
- [Protocol & internals](/docs/building-with-pay/subscriptions/protocol) — see what the 402 challenge and activation credential actually look like on the wire.
