Self-hosting
AgentGuard is one Node process with no framework. Node ≥ 22.6 runs the TypeScript directly (--experimental-strip-types).
Local (SQLite)
git clone https://github.com/Godwin-web3/agentguard && cd agentguard
npm ci && npm run build:web
cp .env.example .env # fill in PayPal sandbox credentials and an admin token (24+ chars)
node --env-file=.env --experimental-strip-types --no-warnings src/server.ts # http://localhost:8787npm start runs the same server with whatever is already in the environment. State lives in ./data (SQLite database AGENTGUARD_DB, issuer key ring AGENTGUARD_KEYRING_FILE). Back it up; the key ring is what makes old receipts verifiable.
Vercel + Postgres
The repo deploys as a single Vercel function (api/index.ts) plus static pages (npm run build:vercel). Set at least:
| Variable | Why |
|---|---|
AGENTGUARD_DATABASE_URL |
Postgres connection string. Apply db/postgres.sql first. Supabase's CA is pinned; set AGENTGUARD_DATABASE_CA for other providers. |
AGENTGUARD_KEYRING_KEY |
Secret (32+ chars) that encrypts the issuer key ring stored in the database. Never stored in the database. |
AGENTGUARD_ADMIN_TOKEN |
Owner token (24+ chars). |
AGENTGUARD_PUBLIC_URL |
Your public origin; also the WebAuthn origin and RP ID. |
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_API_BASE |
Your PayPal app (sandbox: https://api-m.sandbox.paypal.com). |
CRON_SECRET |
Bearer token Vercel sends to /api/cron/*. |
A serverless function has no timers, so background work is exposed as two routes that need Authorization: Bearer $CRON_SECRET:
| Route | Does | Intended schedule |
|---|---|---|
/api/cron/anchor |
timestamps pending receipts, anchors the audit-chain head | every 15 min |
/api/cron/maintenance |
finalizes passkey recoveries, retries notifications, converges stuck payments with PayPal | every 5 min |
Vercel Hobby allows only daily crons, so vercel.json runs each once a day (04:00 and 04:30 UTC) as a fallback and the real schedule comes from Supabase pg_cron + pg_net, with the secret kept in Supabase Vault:
create extension if not exists pg_cron; create extension if not exists pg_net;
select vault.create_secret('<CRON_SECRET>', 'agentguard_cron_secret');
select cron.schedule('agentguard-anchor', '*/15 * * * *', $$
select net.http_post(url := 'https://<domain>/api/cron/anchor',
headers := jsonb_build_object('Authorization', 'Bearer ' || (select decrypted_secret from vault.decrypted_secrets where name = 'agentguard_cron_secret')),
timeout_milliseconds := 60000) $$);
-- same for 'agentguard-maintenance', '*/5 * * * *', /api/cron/maintenanceServerless instances share state through Postgres; reserve, verify, capture and the audit chain run in one transaction under pg_advisory_xact_lock. Free Supabase projects pause after a week without traffic (the maintenance job keeps them awake), and the Vercel function has a 60 s ceiling, so TSA calls use short timeouts.
All variables are listed in Configuration.
Health
GET /healthz returns {ok, db, db_ms} after a database round-trip (503 if it fails) and reveals no configuration. GET /health returns service status.
Upgrading
The SQLite schema is created and migrated on boot. On Postgres, apply db/postgres.sql (idempotent) with psql "$AGENTGUARD_DATABASE_URL" -f db/postgres.sql before deploying. Pause first if you want zero in-flight payments:
curl -s -X POST $U/v1/control/pause -H "authorization: Bearer $T" -H 'content-type: application/json' -d '{"reason":"upgrade"}'
# deploy, then
curl -s -X POST $U/v1/control/resume -H "authorization: Bearer $T"