pay.sh docs
Building with paySubscriptions

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

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
monthly.yml

Serve it:

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:

# 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

FieldRequiredDescription
periodYesBilling period as count + unit. Accepts d/day/days or w/week/weeks. Example: 30d, 2w.
price_usdYesPer-period charge in USD. Converted to mint base units at challenge time.
currencyYesMint symbol (USDC, USDT, …) or base58 mint address. Resolved against the operator's currencies map.
plan_idNo*Base58 of the on-chain Plan PDA. Written back by pay --sandbox server start. Required at runtime.
expires_atNoRFC 3339 timestamp. After this point the server stops renewing and returns fresh 402 challenges. HTTP-layer cap only.
pullerNoBase58 of the wallet authorized to submit transfer_subscription. Defaults to the operator's pubkey.
recipientNoBase58 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 daysN * 24 hours
  • Nw / N week / N weeksN * 168 hours

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

Spec valueMapped hoursOK?
1d24
30d720
2w336
52w8736
1yn/a✗ unsupported unit
1mn/a✗ unsupported unit
400d9600✗ 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.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:

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: USDC

Next

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

On this page