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.
/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;
startSignerSidecarrejects 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