Skip to main content

Vault API

The Vault API handles all signing operations — EVM transactions, EIP-712 typed data, Solana transactions, and the approval/rejection flow.

Sign Transaction (EVM)

Sign and optionally broadcast an EVM transaction. The transaction is evaluated against the agent’s policies before signing.
POST /vault/:agentId/sign
Auth: Agent JWT Request Body:
{
  to: string;          // Destination address (0x...)
  value: string;       // Wei amount as string
  data?: string;       // Calldata (hex string)
  chainId?: number;    // Chain ID (default: server's active chain, typically 8453)
  broadcast?: boolean; // Broadcast after signing (default: true)
}
Success Response (200):
{
  "ok": true,
  "data": {
    "txId": "550e8400-e29b-41d4-a716-446655440000",
    "txHash": "0x8d7592b1a2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7"
  }
}
Pending Approval Response (202):
{
  "ok": false,
  "error": "Transaction requires manual approval",
  "data": {
    "txId": "550e8400-...",
    "results": [
      { "type": "auto-approve-threshold", "passed": false, "reason": "Value exceeds threshold" }
    ],
    "status": "pending_approval"
  }
}
Policy Denied Response (403):
{
  "ok": false,
  "error": "Transaction rejected by policy",
  "data": {
    "txId": "550e8400-...",
    "results": [
      { "type": "spending-limit", "passed": false, "reason": "Exceeds daily limit" }
    ]
  }
}
const result = await steward.signTransaction("my-agent", {
  to: "0xDEX_ROUTER",
  value: "50000000000000000",
  chainId: 8453,
});

if ("txHash" in result) {
  console.log("Broadcast:", result.txHash);
} else if ("status" in result) {
  console.log("Queued for approval");
}

Wallet Actions

Privy-style wallet-action routes expose quote/create/status flows over the existing vault and policy engine.

Quote EVM Transfer

POST /vault/:agentId/actions/transfer/quote
Auth: Owner/admin browser session with recent MFA Quotes accept the same body as transfer creation:
{
  to: string;
  value?: string;     // Wei amount as string
  amountWei?: string; // Alias for value
  token?: "native" | string; // ERC20 token address for token transfers
  chainId?: number;
  broadcast?: boolean;
  referenceId?: string;
  sponsor?: boolean;
}

Create EVM Transfer

POST /vault/:agentId/actions/transfer
Auth: Agent JWT with the required signer authorization for wallet actions Native and selector-gated ERC20 transfer actions return:
  • 200 with status signed or broadcast when policy allows signing.
  • 202 with status pending_approval when policy requires manual approval.
  • 403 with status rejected when policy denies the action.
  • 429 when policy rate limits reject the action.
  • 500 or 502 when signing or RPC submission fails; the error body includes data.actionId when an action row was created.
Broadcasting actions require idempotency. Set a stable referenceId for caller-side dedupe and transaction lookup. ERC20 transfers require an enabled contract-allowlist policy for the token contract and transfer(address,uint256) selector (0xa9059cbb) with recipient allow/block constraints plus maxAmount. Approved ERC20 actions sign or broadcast against the token contract with zero native value, persist token/recipient/amount action metadata, and reject missing allowlists or over-limit amounts before signing. Settled token balance/spend accounting and confirmation polling remain follow-up work.

Create Send-Calls Intent

POST /vault/:agentId/actions/send-calls
Auth: Agent JWT with the required signer authorization for wallet actions Send-calls creates a batch-call wallet-action intent. Today it resolves to status pending_approval or rejected; execution continues through the manual approval/intents workflow. Calldata-bearing calls are rejected unless selector-specific policy extraction or the explicit unsafe signing opt-in is configured.

Get Transfer Action Status

GET /vault/:agentId/actions/:actionId
Auth: Agent JWT plus owner/admin browser session with recent MFA This status endpoint is transfer-only and returns status pending_approval, rejected, signed, broadcast, or failed. Send-calls status is available through transaction and intent history until a dedicated status route is added.

Sign Typed Data (EIP-712)

Sign structured data using eth_signTypedData_v4. Used for DEX approvals, ERC-20 permits, and other typed signatures.
POST /vault/:agentId/sign-typed-data
Auth: Agent JWT Request Body:
{
  domain: {
    name?: string;
    version?: string;
    chainId?: number;
    verifyingContract?: string;
  };
  types: Record<string, Array<{ name: string; type: string }>>;
  primaryType: string;
  value: Record<string, unknown>;
}
Response:
{
  "ok": true,
  "data": {
    "signature": "0x...",
    "txId": "550e8400-..."
  }
}
const result = await steward.signTypedData("my-agent", {
  domain: {
    name: "USDC",
    version: "2",
    chainId: 8453,
    verifyingContract: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  },
  types: {
    Permit: [
      { name: "owner", type: "address" },
      { name: "spender", type: "address" },
      { name: "value", type: "uint256" },
      { name: "nonce", type: "uint256" },
      { name: "deadline", type: "uint256" },
    ],
  },
  primaryType: "Permit",
  value: {
    owner: "0x742d35Cc...",
    spender: "0xDEX_ROUTER",
    value: "1000000",
    nonce: 0,
    deadline: Math.floor(Date.now() / 1000) + 3600,
  },
});

Sign Solana Transaction

Sign a serialized Solana transaction and optionally broadcast it.
POST /vault/:agentId/sign-solana
Auth: Agent JWT Request Body:
{
  transaction: string; // Base64-encoded serialized Solana transaction
  to: string;          // Recipient address (for policy evaluation)
  value: string;       // Lamports as string (for policy evaluation)
  chainId?: number;    // 101 = mainnet, 102 = devnet (default: 101)
  broadcast?: boolean; // Broadcast after signing (default: true)
}
Response:
{
  "ok": true,
  "data": {
    "txId": "550e8400-...",
    "signature": "5KtP2dN8...",
    "broadcast": true,
    "chainId": 101,
    "caip2": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
  }
}

Sign Bitcoin PSBT

Sign a scoped Bitcoin PSBT for an agent wallet. The route signs matching wallet inputs only; it does not broadcast the transaction.
POST /vault/:agentId/sign-bitcoin-psbt
Auth: Agent JWT with signer authorization for sign_transaction, or tenant owner/admin session with recent MFA where configured. Request Body:
{
  walletScope: string;  // e.g. "bitcoin:testnet:p2wpkh:0:0:0"
  psbtBase64: string;   // Base64-encoded PSBT
  finalize?: boolean;   // Return raw tx metadata when the signed PSBT is finalizable
  referenceId?: string; // Caller-supplied tracking ID for audit/history lookup
}
Success Response (200):
{
  "ok": true,
  "data": {
    "transactionId": "550e8400-e29b-41d4-a716-446655440000",
    "signedPsbtBase64": "cHNidP8BA...",
    "signedInputs": 1,
    "walletScope": "bitcoin:testnet:p2wpkh:0:0:0",
    "walletAddress": "tb1q...",
    "addressType": "p2wpkh",
    "network": "testnet",
    "finalizedTxHex": "020000000001...",
    "txId": "4f3c...",
    "vsize": 141,
    "feeSats": "280"
  }
}
transactionId is Steward’s transaction record ID. When finalize is true and every input can be finalized, the response may also include finalizedTxHex, Bitcoin txId, vsize, and feeSats. Steward still does not broadcast that raw transaction; submit it through your own Bitcoin broadcaster or indexer integration. Before signing, Steward decodes standard destination outputs and evaluates the agent’s spend and address policies. It also enforces Bitcoin raw-signing-chain policy requirements and fee caps before returning a signed PSBT or finalized raw transaction metadata.

Safe retry guidance

Provide a stable referenceId for your business operation and an Idempotency-Key header when your deployment enforces or accepts idempotency on sensitive mutations. On network timeouts, retry the same PSBT with the same caller reference and idempotency key, then query transaction history by referenceId before creating a replacement request. If you requested finalize: true, treat a returned finalizedTxHex as a broadcast-ready artifact that may already have been handed to your own broadcasting layer. Do not blindly retry downstream broadcast attempts without checking your broadcaster or indexer for the Bitcoin txId; retrying a finalized raw transaction is different from retrying Steward PSBT signing.
const result = await steward.signBitcoinPsbt("my-agent", {
  walletScope: "bitcoin:testnet:p2wpkh:0:0:0",
  psbtBase64,
  finalize: true,
  referenceId: "order-123-bitcoin-withdrawal",
});

console.log("Steward transaction:", result.transactionId);

if (result.finalizedTxHex) {
  // Steward does not broadcast. Submit through your Bitcoin broadcaster after
  // checking whether result.txId has already been broadcast for this reference.
  console.log("Ready to broadcast:", result.txId);
}

Encrypted Private Key Import

Encrypted import lets a tenant owner/admin replace an agent wallet key without sending a top-level plaintext privateKey field. The import session is one-time, short-lived, stored as an encrypted auth-store record, and bound to the selected tenant, agent, and chain.
POST /vault/:agentId/import/init
POST /vault/:agentId/import/submit
Auth: Tenant owner/admin browser session with recent MFA. API-key automation is rejected when the sensitive-action MFA policy is enabled. Feature flags: STEWARD_ALLOW_PRIVATE_KEY_IMPORT=true and STEWARD_ALLOW_VAULT_PRIVATE_KEY_IMPORT=true

Initialize

{
  "chain": "evm"
}
chain must be "evm" or "solana". The response returns the server X25519 public key, expiry, and AAD fields the client must bind into encryption:
{
  "ok": true,
  "data": {
    "importSessionId": "wimp_...",
    "publicKey": "base64url-spki",
    "algorithm": "X25519-HKDF-SHA256-AES-256-GCM",
    "expiresAt": "2026-06-05T12:00:00.000Z",
    "aad": {
      "importSessionId": "wimp_...",
      "tenantId": "tenant_123",
      "agentId": "my-agent",
      "chain": "evm"
    }
  }
}

Submit

Encrypt the raw private key client-side with X25519-HKDF-SHA256-AES-256-GCM. Use the returned importSessionId as the AES-GCM AAD. The HKDF info string is:
steward:vault-import:v1:{tenantId}:{agentId}:{chain}:{importSessionId}
Submit only the encrypted envelope:
{
  "importSessionId": "wimp_...",
  "ephemeralPublicKey": "base64url-spki",
  "iv": "base64url-12-byte-nonce",
  "ciphertext": "base64url-ciphertext",
  "tag": "base64url-16-byte-tag"
}
Wrong-agent submits return 400 and do not consume the original session. A later submit to the original agentId can still succeed. Once the tenant, agent, chain, envelope shape, and decrypted key validate, Steward atomically consumes the session before writing the new encrypted wallet. Replays after a successful submit return 400 with an invalid-or-expired session error. Session records are serialized through Steward’s KeyStore envelope and stored through the shared auth StoreBackend: Redis when configured, otherwise Postgres auth_kv_store when migrations are available, otherwise an in-memory local-development fallback. This is not an HSM-backed import-session store. Production deployments should configure Redis or run migrations so init and submit can land on different API replicas. /ready reports checks.importSessionStore.source and fails production readiness when the source is memory unless STEWARD_ALLOW_MEMORY_IMPORT_SESSION_STORE=true. Memory fallback is not durable across restarts or processes. Plaintext privateKey fields, malformed envelopes, expired sessions, tenant or agent mismatches, and failed decryptions fail closed. Audit and webhook metadata are redacted and never include the plaintext key or ciphertext.
const session = await steward.initializeEncryptedAgentKeyImport("my-agent", "evm");

await steward.submitEncryptedAgentKeyImport("my-agent", {
  importSessionId: session.importSessionId,
  ephemeralPublicKey,
  iv,
  ciphertext,
  tag,
});
Success Response (200):
{
  "ok": true,
  "data": {
    "agentId": "my-agent",
    "walletAddress": "0x...",
    "chain": "evm"
  }
}

Approve Transaction

Manually approve a pending transaction from the approval queue.
POST /vault/:agentId/approve/:txId
Auth: Tenant API key (agents cannot approve their own transactions) Response:
{
  "ok": true,
  "data": {
    "txId": "550e8400-...",
    "txHash": "0x8d7592b..."
  }
}

Reject Transaction

Reject a pending transaction.
POST /vault/:agentId/reject/:txId
Auth: Tenant API key

Pending Approvals

List transactions awaiting manual approval.
GET /vault/:agentId/pending
Auth: Agent JWT or tenant key Response:
{
  "ok": true,
  "data": [
    {
      "queueId": "queue-uuid",
      "status": "pending",
      "requestedAt": "2026-03-26T15:08:00Z",
      "transaction": {
        "id": "550e8400-...",
        "toAddress": "0xDEX_ROUTER",
        "value": "100000000000000000",
        "chainId": 8453,
        "policyResults": [/* ... */]
      }
    }
  ]
}

Transaction History

List all transactions for an agent.
GET /vault/:agentId/history
Auth: Agent JWT or tenant key
const history = await steward.getHistory("my-agent");

List Transactions

Returns full transaction records for an agent with pagination and filters.
GET /vault/:agentId/transactions
Auth: Agent JWT or tenant key Query Parameters:
{
  status?: "pending" | "approved" | "rejected" | "signed" | "broadcast" | "confirmed" | "failed";
  actionType?: string;
  txHash?: string;
  referenceId?: string; // also accepts reference_id
  limit?: number;
  offset?: number;
}
referenceId filters transactions whose action payload contains either referenceId or reference_id. This matches transfer, send-calls, user-operation, EIP-7702 authorization, raw-signing, and Bitcoin PSBT signing actions that store caller-provided reference IDs.
const result = await steward.listTransactions("my-agent", {
  actionType: "transfer",
  referenceId: "customer-order-123",
  limit: 25,
});

Get Transaction

Returns one full transaction record by ID.
GET /vault/:agentId/transactions/:txId
Auth: Agent JWT or tenant key
const tx = await steward.getTransaction("my-agent", "tx_123");

Get Addresses

List all wallet addresses across chain families.
GET /vault/:agentId/addresses
Auth: Agent JWT or tenant key
const addresses = await steward.getAddresses("my-agent");

Break-Glass Plaintext Key Import

Import an existing private key into the vault.
POST /vault/:agentId/import
Auth: Tenant owner/admin browser session with recent MFA. Both STEWARD_ALLOW_PRIVATE_KEY_IMPORT=true and STEWARD_ALLOW_VAULT_PRIVATE_KEY_IMPORT=true must be enabled. Request Body:
{
  privateKey: string;  // Raw private key
  chain: "evm" | "solana";
}
Prefer the encrypted import session for hosted and browser-based flows. Plaintext import is a break-glass server-side route for controlled operator environments only. The imported key is immediately encrypted, the response is no-store, and the plaintext is never stored or logged.

RPC Passthrough

Proxy read-only RPC calls to the appropriate chain provider.
POST /vault/:agentId/rpc
Auth: Agent JWT Request Body:
{
  method: string;     // RPC method (e.g., "eth_getBalance")
  params?: unknown[]; // Method parameters
  chainId: number;    // Target chain
}
const result = await steward.rpcPassthrough("my-agent", {
  method: "eth_getBalance",
  params: ["0x742d35Cc...", "latest"],
  chainId: 8453,
});