StableOps

Signer

Validate short-lived execution grants before controlled EVM or Solana signing with a local key or AWS KMS.

@stableops/agent-signer is a customer-hosted signing component. It does not accept arbitrary messages. It first validates a short-lived StableOps execution grant, then signs the matching EIP-3009, Permit2, or Solana payment payload for the selected network.

The grant binds the Agent, wallet, network, asset, recipient, amount, nonce, and validity window. A mismatch, expiration, unknown issuer key, or replay conflict is rejected before the wallet key is used.

Wallet pairing and payment signing are separate

During wallet registration, trusted operator code obtains a one-time challenge through the management client and signs the complete challenge with the wallet account's signMessage. Only after registration does the signer sidecar process payment execution grants.

The sidecar's /v1/sign endpoint accepts only StableOps execution grants. It cannot sign wallet-pairing challenges or arbitrary messages. Do not add a generic signing endpoint for convenience.

See quickstart section 2.3 for the wallet registration flow.

Local test signer

LocalTestSigner is limited to Sandbox EVM test networks. Set its environment and CAIP-2 network so every execution grant is bound to the intended environment and chain. Use AwsKmsSigner for Live EVM networks.

  • the wallet key matching the registered payment address;
  • the current StableOps execution-grant key ID and Ed25519 public key;
  • a high-entropy sidecar authentication token; and
  • a durable, access-restricted grant authorization store.
import { readFileSync } from 'node:fs'
import {
  FileGrantAuthorizationStore,
  LocalTestSigner,
  startSignerSidecar,
} from '@stableops/agent-signer'

function required(name: string): string {
  const value = process.env[name]?.trim()
  if (!value) throw new Error(`Missing environment variable ${name}`)
  return value
}

const signer = new LocalTestSigner({
  privateKey: required('BASE_SEPOLIA_TEST_PRIVATE_KEY') as `0x${string}`,
  environment: 'SANDBOX',
  network: 'eip155:84532',
  grantVerification: {
    publicKeys: {
      [required('STABLEOPS_GRANT_KEY_ID')]: readFileSync(
        required('STABLEOPS_GRANT_PUBLIC_KEY_FILE'),
        'utf8',
      ),
    },
  },
  store: new FileGrantAuthorizationStore('./data/grant-authorizations.json'),
})

const { url } = await startSignerSidecar({
  signer,
  host: '127.0.0.1',
  port: 8789,
  authToken: required('STABLEOPS_SIDECAR_TOKEN'),
})

console.log(`Signer sidecar listening at ${url}`)

The sidecar exposes GET /health and POST /v1/sign. startSignerSidecar rejects non-loopback listen addresses. Still require an authentication token so other local processes cannot call the signing endpoint.

FileGrantAuthorizationStore preserves used grants across restarts. Do not put it in a temporary directory or share one local file among signer replicas without concurrency coordination. Multi-replica deployments need a shared store implementation with atomic writes and uniqueness constraints.

For Solana, use LocalSvmSigner.create() with a Base58-encoded Ed25519 private key and explicitly bind it to Solana mainnet or Devnet:

import { readFileSync } from 'node:fs'
import {
  FileGrantAuthorizationStore,
  LocalSvmSigner,
} from '@stableops/agent-signer'

function required(name: string): string {
  const value = process.env[name]?.trim()
  if (!value) throw new Error(`Missing environment variable ${name}`)
  return value
}

const signer = await LocalSvmSigner.create({
  privateKey: required('SOLANA_PRIVATE_KEY_BASE58'),
  environment: 'LIVE',
  network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
  grantVerification: {
    publicKeys: {
      [required('STABLEOPS_GRANT_KEY_ID')]: readFileSync(
        required('STABLEOPS_GRANT_PUBLIC_KEY_FILE'),
        'utf8',
      ),
    },
  },
  store: new FileGrantAuthorizationStore('./data/grant-authorizations.json'),
})

The facilitator fee payer covers the Solana transaction fee. The payment wallet does not need SOL for the x402 payment itself.

AWS KMS signer

A Live EVM topology should keep the private key in AWS KMS. AwsKmsSigner requires a SIGN_VERIFY key using ECC_SECG_P256K1 with ECDSA_SHA_256:

import { readFileSync } from 'node:fs'
import {
  AwsKmsSigner,
  FileGrantAuthorizationStore,
} from '@stableops/agent-signer'

function required(name: string): string {
  const value = process.env[name]?.trim()
  if (!value) throw new Error(`Missing environment variable ${name}`)
  return value
}

const signer = await AwsKmsSigner.create({
  keyId: required('STABLEOPS_KMS_KEY_ID'),
  walletAddress: required('STABLEOPS_WALLET_ADDRESS') as `0x${string}`,
  environment: 'LIVE',
  network: 'eip155:8453',
  clientConfig: { region: process.env.AWS_REGION },
  grantVerification: {
    publicKeys: {
      [required('STABLEOPS_GRANT_KEY_ID')]: readFileSync(
        required('STABLEOPS_GRANT_PUBLIC_KEY_FILE'),
        'utf8',
      ),
    },
  },
  store: new FileGrantAuthorizationStore('./data/grant-authorizations.json'),
})

At startup, the signer retrieves the KMS public key and derives its EVM address. It fails if that address differs from walletAddress. KMS receives a digest with MessageType: DIGEST; the signer also validates DER output, normalizes high-s signatures, and verifies the recovered address.

AWS KMS supports EVM networks only. Live payments also require the organization mainnet risk gates. Use a separate signer instance, wallet, authorization store, and explicit network value for each network; never let one instance accept grants for multiple networks.

Deployment checklist

  • Bind the sidecar to loopback only; startSignerSidecar rejects non-loopback addresses. Never expose it publicly.
  • Give the Agent runtime only the sidecar URL and token, not the wallet key or KMS administration rights.
  • Obtain grant verification keys from trusted onboarding material and rotate key ID and public key together.
  • Persist and back up replay records without allowing replicas to bypass uniqueness guarantees.
  • Log identifiers and error categories, never private keys, full grants, or payment signatures.

How is this guide?

Last updated

On this page