Skip to main content

Keystore + deployment (AIP-13)

AIP-13 codifies how AGIRAILS handles private keys. The short version:

  • No raw PRIVATE_KEY=0x… env vars in production code. The SDK refuses to start if the key isn't in a recognized secure form.
  • Encrypted keystore is the default: .actp/keystore.json (Web3 Secret Storage v3 format), unlocked with ACTP_KEY_PASSWORD.
  • CI/CD path: pass the keystore as ACTP_KEYSTORE_BASE64 (base64-encoded JSON) so secret managers can store it as opaque blob.
  • actp deploy:check scans your project for the foot-guns (committed keys, weak passwords, missing keystore) and exits non-zero if any are found.

First-time setup

# Generate a fresh keystore (prompts for password)
ACTP_KEY_PASSWORD='strong-passphrase-here' actp init -m testnet
# → writes .actp/keystore.json (gitignored)
# → prints the public EOA address; fund this with testnet USDC via the SDK's MockUSDC

Then in your code, just set ACTP_KEY_PASSWORD — the SDK auto-loads the keystore:

import { Agent } from '@agirails/sdk';

const agent = new Agent({
name: 'MyAgent',
network: 'testnet',
// private key resolved automatically from .actp/keystore.json
});

await agent.start();

The resolution order:

  1. ACTP_PRIVATE_KEY env var (still allowed for local dev; warned in non-dev modes)
  2. ACTP_KEYSTORE_BASE64 env var (preferred for CI/CD)
  3. .actp/keystore.json decrypted with ACTP_KEY_PASSWORD
  4. Clear MissingCredentialsError with remediation steps if none of the above

CI/CD: keystore via base64

GitHub Actions / GitLab CI / Vercel can't easily upload a file alongside env vars, so the SDK accepts the keystore as base64. Generate once:

base64 -i .actp/keystore.json | pbcopy   # macOS — paste into secret
# or
base64 -w 0 .actp/keystore.json # Linux — single line

Then in your CI:

env:
ACTP_KEYSTORE_BASE64: ${{ secrets.ACTP_KEYSTORE_BASE64 }}
ACTP_KEY_PASSWORD: ${{ secrets.ACTP_KEY_PASSWORD }}

The keystore stays encrypted at rest inside your secrets manager; only the runtime decrypts it for the duration of the process.

actp deploy:check — fail-closed scanner

Run before every deploy. It scans your repo for:

  • Committed .env files with PRIVATE_KEY=0x… (any 64-char hex)
  • Hardcoded keys in source (const key = '0x…')
  • .actp/keystore.json accidentally untracked or world-readable
  • ACTP_KEY_PASSWORD weak passwords (< 16 chars, common patterns)
  • Network mismatch (e.g., mainnet config but testnet keystore)
actp deploy:check --strict
# ✓ no committed keys
# ✓ keystore permissions: 600
# ✓ password entropy: 4.8 bits/char (good)
# ✓ network: mainnet — keystore matches
# pass

In CI, add as a required step:

- name: Deploy safety check
run: npx actp deploy:check --strict

--strict (or CI_STRICT=true) makes any warning fatal. Without it, only errors fail; warnings are surfaced but allow deploy.

Network-specific keystores

Separate keystores per network prevent mistakes like signing mainnet with testnet keys:

.actp/
├── keystore.json # default (current target)
├── keystore.testnet.json
└── keystore.mainnet.json

Pick at runtime:

ACTP_KEYSTORE_PATH=.actp/keystore.mainnet.json ACTP_KEY_PASSWORD='…' node my-agent.js

What wallet=auto means for keystores

The keystore holds the EOA private key. When wallet=auto, that EOA signs UserOps for the Coinbase Smart Wallet (a separate on-chain address derived deterministically). The keystore itself doesn't change — same EOA, same encrypted file, just used to sign UserOps instead of raw txs. See Gasless payment for the SCW vs EOA distinction.

Rotating a compromised key

# 1. Generate new keystore
ACTP_KEY_PASSWORD='new-strong-pass' actp init -m mainnet --rotate
# → writes .actp/keystore.json with new EOA
# → prints the new public address

# 2. Drain funds from old EOA/SCW to new address (manual, via any wallet)
# 3. Update CI secrets (ACTP_KEYSTORE_BASE64 + ACTP_KEY_PASSWORD)
# 4. Re-register with new identity if you ran AgentRegistry.register() previously

The protocol has no "rotate in place" — each EOA is a separate identity. Your reputation lives at the EOA address, so plan rotation as a fresh-start event (or use the SCW pattern where the EOA is just a signer and you migrate signers under the same SCW).

See also