docs / deploy

view as .md

Testnet deployment runbook#

Internal proof runbook — not a public offering. Grantor's public product is the sovereign tier (no server). This runbook exists to prove the full stack end-to-end on a testnet; see docs/strategy/2026-07-30-sovereign-only-positioning.md.

Deploy the whole Grantor stack — contract, tiers, a tenant, and the live issuer — to an L2 testnet. Cost ≈ $0 (testnet gas from a faucet; MockUSDC minted free). ~20 minutes.

Example chain below: Base Sepolia (RPC=https://sepolia.base.org). Any EVM testnet works (Arbitrum/OP Sepolia, Ethereum Sepolia).

0. Prerequisites#

  • Foundry (forge, cast, anvil) and Docker and jq.
  • A deployer key funded with a little testnet ETH:
    • Create one: cast wallet new (save the key + address).
    • Fund it from a faucet — Coinbase Developer Platform, Alchemy, or a PoW faucet for your chain. (Some gate on a tiny mainnet balance; pick one that doesn't, or park ~$2 of mainnet ETH in the wallet as a gate — unspent.)
  • An RPC URL (public, or Alchemy/Infura free tier).

1. Configure .env#

cp .env.example .env

Set at least:

PRIVATE_KEY=0x<deployer key>        # deployer == registry owner (needed for tier config)
RPC=https://sepolia.base.org
DOMAIN=http://localhost:8080        # your issuer URL (a tunnel later; localhost for now)
TENANT_ID=1                         # the FIRST createTenant returns id 1
TENANT_SALT=<long random secret>
CLIENTS_FILE=./clients.toml         # cp clients.example.toml clients.toml and edit
# TREASURY / GRACE_WINDOW optional — TREASURY defaults to the deployer.

Generate the issuer signing key and paste it into ISSUER_SIGNING_KEY:

openssl ecparam -genkey -name prime256v1 -noout | openssl pkcs8 -topk8 -nocrypt

2. Deploy the contracts#

just deploy-testnet "$RPC"

Deploys MockUSDC + GrantorRegistry and configures the Hobby/Pro/Scale tiers. Note the logged addresses; put them in .env:

CONTRACT_ADDR=0x<GrantorRegistry>
USDC=0x<MockUSDC>

(Optional) verify on the explorer for transparency:

cd contracts && forge verify-contract $CONTRACT_ADDR src/GrantorRegistry.sol:GrantorRegistry \
  --chain base-sepolia --watch   # needs an explorer API key

3. Run the issuer#

just docker-build
docker run -d --name grantor --env-file .env \
  -e CONTRACT_ADDR=$CONTRACT_ADDR \
  -v $PWD/clients.toml:/etc/grantor/clients.toml:ro \
  -p 8080:8080 grantor-issuer

It boots without touching the chain. Sanity:

curl -s localhost:8080/healthz                                  # -> ok
curl -s localhost:8080/.well-known/openid-configuration | jq .  # issuer metadata

4. Register your tenant + issuer key#

Grab the issuer key id from the running issuer, then stand up the tenant:

export ISSUER_KEY_ID=$(just issuer-keyid http://localhost:8080)   # the JWKS kid == on-chain keyId
echo "ISSUER_KEY_ID=$ISSUER_KEY_ID" >> .env
just setup-tenant "$RPC"

This createTenant (id 1), mints + topUps MockUSDC, drawPeriods to Active, and registerIssuerKeys your key. The log shows issuer key active true. Since .env already has TENANT_ID=1, the issuer is now fully live — its isActive reads will return true.

5. (Grant C) Register an agent key#

For agent tokens, enroll the agent's address:

cast send $CONTRACT_ADDR "registerAgentKey(uint256,address)" 1 0x<agentAddress> \
  --rpc-url "$RPC" --private-key "$PRIVATE_KEY"

6. Validate end-to-end#

Agent token (Grant C) — with the Python SDK (the agent key = the address you enrolled):

from grantor_sdk import get_agent_token, verify_token
tok = get_agent_token(issuer="http://localhost:8080", tenant_id=1,
                      audience="resource-api", secret_key_hex="<agent key>")
claims = verify_token(tok["access_token"], issuer="http://localhost:8080", audience="resource-api")
print(claims.sub, claims.tid)   # pairwise sub, tid == 1

This exercises the full path: SDK signs → issuer verifies the agent key on your live testnet contract → mints → SDK verifies via JWKS.

Billing lifecycle (optional): let the period lapse, watch isActive go Active → Grace → Inactive, then topUp + drawPeriod to reactivate. The issuer picks up the change within its cache TTL.

7. Public URL (optional)#

For a shareable demo, expose the issuer over HTTPS with a free tunnel and set DOMAIN to the tunnel URL (then restart the container so SIWE/domain checks match):

cloudflared tunnel --url http://localhost:8080    # or: ngrok http 8080

Anonymity notes#

  • Fund the deployer key from a fresh source; don't reuse a doxxed wallet.
  • Host/domain/RPC accounts should preserve operator pseudonymity if that's the goal (this is the place to rehearse it).
  • The git identity is still grantor-dev — rename before pushing anything.

Teardown#

docker rm -f grantor

The testnet contracts stay on-chain (harmless). Redeploy anytime with just deploy-testnet.

→ Production#

Same flow on an L2 mainnet, with two changes: point USDC at the chain's real USDC (don't deploy MockUSDC; drop the mint in SetupTenant and topUp from your balance), and transfer registry ownership to a multisig/timelock after tier config. See the cost estimate in the design discussion (~$10–30 one-time + ~$10–40/mo; the on-chain fees are net-positive at real usage).

This page is also served as Markdown — agents should read that. The whole tree is indexed for machines in llms.txt.