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. This page covers only the subscription: block.
Agent summary
subscription:andmetering:are mutually exclusive on the same endpoint.periodaccepts a count plusd/day/daysorw/week/weeks.monthis rejected.- The mapped per-period interval must fall in
[1h, 8760h]. Up to one year per period. plan_idis written back into the file bypay --sandbox server starton first launch.- Operator-level
recipientandpullerdefaults apply unless overridden per endpoint.
Minimal example
The smallest complete provider spec with one subscription endpoint — $9.99 USDC every 30 days.
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: USDCServe it:
pay --sandbox server start monthly.ymlThe 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:
# 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/feedField 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 * 24hoursNw/N week/N weeks→N * 168hours
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:
pay --sandbox server start provider.ymlFor 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) › noAnswer 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:
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:
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: USDCNext
- Examples — monthly, weekly, annual, multi-tier — six runnable specs you can download and serve.
- Protocol & internals — see what the 402 challenge and activation credential actually look like on the wire.