# 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](https://vercel.com/docs/functions/container-images). 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:

```txt
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.

```yaml title="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 title="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 title="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`:

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

## Environment variables

Set the runtime values as Vercel environment variables:

```sh
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:

```sh
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:

```sh
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:

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

Check process health:

```sh
curl -fsS "$SERVICE_URL/__402/health"
```

Check the OpenAPI document:

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

Call a paid endpoint through `pay`:

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

Read deployment logs when startup or payment verification fails:

```sh
vercel logs "$SERVICE_URL"
```

## Troubleshooting

| Symptom                                              | Likely cause                                                     | Fix                                                                           |
| ---------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Vercel returns 502 or times out before health checks | The container did not bind to Vercel's expected port             | Use the current `latest` image so the gateway reads `$PORT`.                  |
| The gateway returns 402 again after payment          | `PAY_MPP_CHALLENGE_SECRET` changed across instances or restarts  | Set one stable secret in Vercel env vars and redeploy.                        |
| `/__402/health` works but your route returns 404     | The path is missing from `endpoints[]` or has a method mismatch  | Add the exact method and path to `provider.yml`, then redeploy.               |
| The upstream returns 401 after payment               | The upstream credential was not injected                         | Check the `routing.auth.value_from_env` name and the matching Vercel env var. |
| Preview URL redirects to Vercel SSO                  | Vercel deployment protection is enabled for preview environments | Use the production alias for probes, or disable protection for that preview.  |
