pay.sh docs
Building with payDeployment

Deploy to Vercel

Deploy pay as a Vercel container image with an env-backed signer, upstream OpenAPI URL, and stable 402 challenge secret.

Vercel can run OCI container images as Vercel Functions. Use this path when you want a low-ops deployment and your paid API fits Vercel Function limits.

Agent summary

  • Put Dockerfile.vercel at the Vercel project root.
  • Use CMD ["server", "start", ...]; no shell entrypoint is needed with current pay images.
  • Pass an upstream OpenAPI or Discovery URL to --openapi when the upstream publishes one.
  • Use operator.signer.backend: env and store PAY_OPERATOR_KEYPAIR in Vercel environment variables.
  • Use the published latest pay image. The container image is not version-tagged yet.
  • Connect the project to Git with Vercel's Container preset so pushes create deployments.
  • Set one stable PAY_MPP_CHALLENGE_SECRET per environment. Paid retries can fail with another 402 if this value changes across cold starts.
  • Use the production alias for agent probes unless your preview deployment is publicly accessible.

What you are deploying

Use this layout for a single-service Vercel gateway:

deploy/
  provider.yml
  Dockerfile.vercel

If the upstream OpenAPI document is not available over https://, add a local openapi.json and copy it into the image.

Provider spec

Use env templates for deploy-time values. operator.signer.backend: env reads PAY_OPERATOR_KEYPAIR at startup and keeps the signer in process memory.

provider.yml
# yaml-language-server: $schema=https://pay.sh/docs-assets/provider.schema.json
name: quotes
subdomain: quotes
title: 'Quote Gateway'
description: 'Paid stock quote endpoint.'
category: finance
version: v1

routing:
  type: proxy
  url: '${UPSTREAM_BASE_URL}'
  auth:
    method: header
    key: Authorization
    value_from_env: UPSTREAM_API_KEY

operator:
  currencies:
    usd: ['USDC']
  network: mainnet
  fee_payer: true
  rpc_url: '${PAY_RPC_URL}'
  recipient: '${PAY_PAYMENT_RECIPIENT}'
  challenge_binding_secret: '${PAY_MPP_CHALLENGE_SECRET}'
  signer:
    backend: env
    value_from_env: PAY_OPERATOR_KEYPAIR

endpoints:
  - method: GET
    path: 'v1/quote/{symbol}'
    description: 'Return a quote for one ticker symbol.'
    metering:
      dimensions:
        - direction: usage
          unit: requests
          scale: 1
          tiers:
            - price_usd: 0.01

PAY_OPERATOR_KEYPAIR must contain a Solana CLI JSON keypair array or a base58-encoded 64-byte keypair. Do not commit the keypair file.

value_from_env takes the environment variable name. Use ${...} templates only for string values that pay expands at startup, such as routing.url, operator.rpc_url, operator.recipient, and operator.challenge_binding_secret.

Container image

Dockerfile.vercel
ARG PAY_IMAGE=ghcr.io/solana-foundation/pay:latest
FROM ${PAY_IMAGE}

WORKDIR /app
COPY provider.yml /app/provider.yml

CMD ["server", "start", "/app/provider.yml", "--openapi", "https://api.example.com/openapi.json"]

This guide relies on the current latest image. Older manually built or cached images may not expand provider-spec env templates or bind to Vercel's PORT environment variable.

When the upstream OpenAPI document is local, copy it into the image:

Dockerfile.vercel
ARG PAY_IMAGE=ghcr.io/solana-foundation/pay:latest
FROM ${PAY_IMAGE}

WORKDIR /app
COPY provider.yml /app/provider.yml
COPY openapi.json /app/openapi.json

CMD ["server", "start", "/app/provider.yml", "--openapi", "/app/openapi.json"]

Multi-service Vercel projects

If the gateway is one service in a larger Vercel project, route traffic with services:

vercel.json
{
  "services": {
    "gateway": {
      "root": ".",
      "entrypoint": "Dockerfile.vercel"
    }
  },
  "rewrites": [{ "source": "/(.*)", "destination": { "service": "gateway" } }]
}

Environment variables

Set the runtime values as Vercel environment variables:

cd deploy
vercel link
vercel env add UPSTREAM_BASE_URL production
vercel env add UPSTREAM_API_KEY production
vercel env add PAY_RPC_URL production
vercel env add PAY_PAYMENT_RECIPIENT production
vercel env add PAY_MPP_CHALLENGE_SECRET production
vercel env add PAY_OPERATOR_KEYPAIR production

Generate PAY_MPP_CHALLENGE_SECRET once per environment:

openssl rand -hex 32

Keep this value stable across redeploys and cold starts. If the value changes between the initial 402 and the paid retry, the gateway rejects the replay and the client sees another 402.

Deploy from Git

Use a Git-connected Vercel project for normal deployments:

  1. Push deploy/provider.yml and deploy/Dockerfile.vercel to your repository.
  2. Import or connect the repository in Vercel.
  3. Select the Container preset.
  4. Set the project root directory to deploy for the single-service layout above.
  5. Add the production environment variables, then deploy.

After the first deployment, Vercel builds the container on every push according to your project branch rules.

For a one-off manual deploy, run:

cd deploy
vercel deploy --prod

Preview URLs can be protected by Vercel SSO depending on your team settings. Use the production alias, or disable protection for a machine-readable preview URL before probing it with agents.

Verify

Set the service URL:

SERVICE_URL="https://quotes.example.com"

Check process health:

curl -fsS "$SERVICE_URL/__402/health"

Check the OpenAPI document:

curl -fsS "$SERVICE_URL/openapi.json" | jq '.openapi // .swagger // .info.title'

Call a paid endpoint through pay:

pay curl "$SERVICE_URL/v1/quote/AAPL"

Read deployment logs when startup or payment verification fails:

vercel logs "$SERVICE_URL"

Troubleshooting

SymptomLikely causeFix
Vercel returns 502 or times out before health checksThe container did not bind to Vercel's expected portUse the current latest image so the gateway reads $PORT.
The gateway returns 402 again after paymentPAY_MPP_CHALLENGE_SECRET changed across instances or restartsSet one stable secret in Vercel env vars and redeploy.
/__402/health works but your route returns 404The path is missing from endpoints[] or has a method mismatchAdd the exact method and path to provider.yml, then redeploy.
The upstream returns 401 after paymentThe upstream credential was not injectedCheck the routing.auth.value_from_env name and the matching Vercel env var.
Preview URL redirects to Vercel SSOVercel deployment protection is enabled for preview environmentsUse the production alias for probes, or disable protection for that preview.

On this page