Ripardocs
Dashboard

Deploy anywhere

There is no ripar deploy and no managed runtime — an agent built with @ripar/sdk is an ordinary Node HTTP server, and hosting it is your call. That is the whole story: it runs anywhere Node runs.

Three properties are what make that true, and every platform below leans on them:

  • It reads PORT. serve() listens on process.env.PORT, falling back to 4021. Never hardcode a port — platforms assign one.
  • It answers /health without payment. So does /.well-known/ripar.json. A health check does not need a wallet.
  • It honours x-forwarded-proto and x-forwarded-host. Behind a platform's TLS terminator, the socket address is not the URL callers reach — the manifest advertises the forwarded one instead.
Nothing here holds a key

Settlement goes from the caller straight to the address in payTo. The process needs no wallet and no signing key, so a compromised container cannot move your money. The only secret worth protecting is whatever your own handler uses.

Before any of them

Build to plain JavaScript and set two variables:

npm run build                 # tsc → dist/
VariableRequiredNotes
RIPAR_PAY_TOYesYour Algorand address. Paste it from the wallet; a retyped address that still checksums sends revenue elsewhere.
RIPAR_NETWORKNomainnet (default) or testnet. Deploy to TestNet first.
PORTNoSet by the platform. Read, never written.
@ripar/sdk is not on npm yet

npm ci on a platform builder will fail to resolve it. Until 0.1.0 is published, vendor the built SDK into your image or install it from git — npm i github:nickthelegend/ripar-sdk — and check that your platform's build step can reach GitHub.

Railway

Nixpacks reads package.json, builds it and injects PORT. There is no config file.

railway init
railway variables set RIPAR_PAY_TO=ADDR…K7QX RIPAR_NETWORK=mainnet
railway up

Set the health check path to /health in Settings → Deploy. Railway terminates TLS and forwards x-forwarded-proto, so the manifest advertises the https:// URL your callers actually reach.

Render

Commit a blueprint and every push redeploys the agent from the same file:

render.yaml
services:
  - type: web
    name: text-tools
    runtime: node
    plan: starter
    buildCommand: npm ci && npm run build
    startCommand: node dist/index.js
    healthCheckPath: /health
    envVars:
      - key: RIPAR_NETWORK
        value: mainnet
      - key: RIPAR_PAY_TO
        sync: false

sync: false keeps the payout address out of the repository — Render asks for it once in the dashboard.

Free instances sleep

A sleeping instance answers its first call after a cold start of several seconds. That is fine for an endpoint nobody is waiting on and wrong for one an agent is timing. Use a paid instance for anything you have listed.

Fly.io

Fly is the one to reach for when latency matters — run the endpoint in the regions its callers are in.

fly.toml
app = "text-tools"
primary_region = "iad"
 
[build]
 
[env]
  RIPAR_NETWORK = "mainnet"
 
[http_service]
  internal_port = 4021
  force_https = true
  auto_stop_machines = "suspend"
  auto_start_machines = true
  min_machines_running = 0
 
  [[http_service.checks]]
    interval = "30s"
    timeout = "5s"
    grace_period = "10s"
    method = "GET"
    path = "/health"
fly launch --no-deploy
fly secrets set RIPAR_PAY_TO=ADDR…K7QX
fly deploy

internal_port must match the port serve() listens on — 4021 unless you set PORT. min_machines_running = 0 scales to zero; raise it to 1 for an endpoint that must never cold-start.

Heroku

A Procfile and a git push. The oldest path here, and still the shortest.

Procfile
web: node dist/index.js
heroku create text-tools
heroku config:set RIPAR_PAY_TO=ADDR…K7QX RIPAR_NETWORK=mainnet
git push heroku main

Heroku assigns PORT at boot, which serve() already reads. Dynos restart at least daily — harmless here, because nothing is held in memory between requests. If your handler caches anything, treat the cache as cold on every boot.

Docker

The escape hatch: anywhere that runs a container runs a paid endpoint.

Dockerfile
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
ENV PORT=4021
EXPOSE 4021
HEALTHCHECK --interval=30s --timeout=3s \
  CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "dist/index.js"]
docker build -t text-tools .
docker run -p 4021:4021 -e RIPAR_PAY_TO=ADDR…K7QX text-tools
Forward the proxy headers

Behind nginx, Caddy or an ingress controller, pass x-forwarded-proto and x-forwarded-host through. Without them the manifest advertises the container's own hostname, and a caller that discovers your endpoint cannot reach it.

nginx.conf
location / {
  proxy_pass http://text-tools:4021;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
}

Prove it works

Two checks, in this order. First, the manifest — free, and what every caller reads before it decides to pay:

curl -s https://your-host/.well-known/ripar.json | jq '{payTo, network, endpoints: [.endpoints[].url]}'

The payTo must be your address and the URLs must be reachable from outside. If they carry a container hostname or http://, the proxy headers are not getting through.

Then the payment gate — an unpaid call must be refused with a price, not served:

curl -i -X POST https://your-host/summarize \
  -H 'content-type: application/json' \
  -d '{"text":"hello"}'
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MiwiYWNjZXB0cyI6W3sic2NoZW1lIjoiZXhhY3Qi…
 
{}

A 200 here means the middleware is not in front of your route, and you are giving the work away. The quote is base64 in the header and the body is {}ripar quote <url> decodes it for you.

Being found once it is up

Self-hosting does not put you in an index, but neither does anything else right now — there is no automatic listing on any deployment path. What a caller can reach is your manifest, so the URL is the thing worth handing out:

https://your-host/.well-known/ripar.json

Bazaar listing needs no call: serve() declares discovery on every listed route and the facilitator catalogues it while verifying a payment. See Discovery for exactly what works today and what does not. Uptime is yours to hold up either way.