[ BLOG ]

Your Cardano Agent Never Holds the Key: HSM Signing on the Intents API

1Claw signs and broadcasts ADA from inside the HSM. Ed25519 signing over a CBOR-encoded transaction body, protocol parameters and UTXOs fetched via Blockfrost automatically. Per-agent guardrails on chains, destinations, and spend caps. The agent never sees the private key.

If you run a Cardano agent, you are probably loading a signing key from a .skey file or an environment variable at startup:

from pycardano import PaymentSigningKey
skey = PaymentSigningKey.load("payment.skey")

Treasury bots, DeFi automation, Catalyst voting agents, NFT marketplace bots. Same pattern everywhere.

The problem with Cardano specifically is worse than it looks on other chains. Cardano uses an extended UTxO model. A wallet is a collection of UTxOs, and a single transaction can consume every one of them simultaneously. On an account-based chain like Ethereum, a leaked key drains value incrementally, one transaction at a time. There is a window to detect the compromise and rotate. On Cardano, one signed transaction and the entire wallet balance is gone in a single block confirmation. No partial failure. No detection window.

We shipped Cardano on the Intents API so agents stop holding keys at all. The agent sends chain, recipient, and amount. The server builds the transaction, signs it in the HSM, and broadcasts. Back comes a tx_hash, not a key.

Getting a signing key

A human provisions the key once. Agents cannot create or rotate keys themselves; those endpoints return 403 on purpose.

curl -X POST "https://api.1claw.xyz/v1/agents/$AGENT_ID/signing-keys" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "chain": "cardano" }'

You get back an Ed25519 public key in hex encoding and a Bech32 enterprise address. On mainnet that is addr1v. On Preprod and Preview testnets it is addr_test1v. The address payload is the blake2b-224 hash of the Ed25519 verification key. The private key goes into the org's __agent-keys vault, encrypted under Cloud KMS. The API never returns the private key.

{
  "chain": "cardano",
  "curve": "Ed25519",
  "address": "addr1v9k5cyknawmt5mcnjn7fy25k8k...",
  "public_key": "a1b2c3d4...",
  "key_version": 1
}

Enterprise addresses do not carry staking credentials. That is the right format for agents that are not delegating stake and need a clean address for smart contract interactions and transfers.

Sending ADA

Signing runs through Ed25519 over the blake2b-256 hash of the CBOR-encoded transaction body. The server fetches protocol parameters, UTXOs, and the current tip slot from Blockfrost automatically. No RPC configuration on your end. Set BLOCKFROST_PROJECT_ID in your environment (or BLOCKFROST_PROJECT_ID_PREPROD and BLOCKFROST_PROJECT_ID_MAINNET for per-network control). Blockfrost's free tier covers 50,000 requests per day.

curl -X POST "https://api.1claw.xyz/v1/agents/$AGENT_ID/transactions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "chain": "cardano-preprod",
    "to": "addr_test1v...",
    "value": "2"
  }'

value is plain ADA as a decimal string. "2"is 2,000,000 lovelace. The server selects UTXOs, calculates the fee using Cardano's formula (min_fee_a * tx_size + min_fee_b), and sets TTL to current_slot + 7200 by default. At one slot per second that gives the transaction roughly two hours to land before it expires automatically. Override it with the ttl field if your use case needs a tighter or looser window.

Cardano requires a minimum ADA value on every output. If you send less than the protocol minimum (currently 1 ADA on mainnet, subject to protocol parameters), the server returns a validation error before the transaction is built. Size your sends accordingly.

UTxOs selected for a transaction are locked in the database while the transaction is in flight and released after broadcast or on error. Two concurrent submits will not select the same inputs.

Chains: cardano (mainnet), cardano-preprod, cardano-preview. Preprod is the primary integration environment. Preview runs shorter epochs and is useful if you need to test epoch boundary behaviour.

ADA transfers are live today. Multi-asset transactions covering Cardano native tokens are on the roadmap. When that ships, agents handling LP tokens, stablecoin positions (DJED, USDM), and CNFTs will use the same intent-based flow.

Where signing runs

api.1claw.xyz uses Cloud KMS HSM in Cloud Run. Key material is decrypted inside the HSM boundary, used once, then cleared from memory.

shroud.1claw.xyz and intents.1claw.xyz run the same flow inside AMD SEV-SNP confidential VMs on GKE. Keys only exist in confidential memory inside the enclave. That is the option if you need attestation or a stricter compliance story.

Both support sign-only via POST /v1/agents/:id/transactions/sign. You get the signed transaction CBOR back and submit it yourself via Blockfrost, a local node, or any Cardano submit endpoint.

The agent never gets the key

Flip intents_api_enabled: true on an agent and two things happen at once: it can call the transaction endpoints, and it gets a 403 if it tries to read private_key or ssh_key secrets through the normal vault API. That is enforced in the handler, not documented as a guideline.

Guardrails run before signing

Blocking key reads closes the obvious exfil path. On Cardano specifically, guardrails matter more than on most chains: the eUTxO sweep risk means the downside of a compromised agent is not a partial drain but a total one. Guardrails cap what a compromised agent can actually do before it reaches the key.

Every Cardano submit runs through guardrail checks in the handler before the HSM unwraps anything. Shroud runs the same checks on shroud.1claw.xyz and intents.1claw.xyz. Configure them once on the agent via dashboard, SDK, or CLI.

  • Allowed chains (tx_allowed_chains): lock an agent to cardano-preprod during dev, or cardano only in prod
  • Destination allowlist (tx_to_allowlist): only pay Bech32 addresses you have pre-approved. Anything else returns 403 at signing time
  • Per-transaction cap (tx_max_value): max ADA in one transaction as a decimal string. "100" = 100 ADA
  • Daily spend cap (tx_daily_limit): rolling 24-hour limit on Cardano-family spend. The server tracks tx_spent_today_by_chain.cardano, not a cross-chain total
  • Per-chain overrides (per_chain_guardrails): set Cardano-specific max_value, daily_limit, and to_allowlist under a cardano key. Strictest of global and per-chain wins

Example: a Preprod treasury agent locked to a single recipient with tight caps:

await client.agents.update(agentId, {
  intents_api_enabled: true,
  tx_allowed_chains: ["cardano-preprod"],
  per_chain_guardrails: {
    cardano: {
      to_allowlist: ["addr_test1vYourAddress..."],
      max_value: "50",
      daily_limit: "500",
    },
  },
});

Prompt injection cannot talk its way around this. The agent submits an intent; the server checks the allowlist and caps; only then does signing happen. If anything fails, you get 403 and nothing hits the chain.

Rotate and revoke

OperationEndpoint
ProvisionPOST /v1/agents/{id}/signing-keys
ListGET /v1/agents/{id}/signing-keys
RotatePOST /v1/agents/{id}/signing-keys/cardano/rotate
DeactivateDELETE /v1/agents/{id}/signing-keys/cardano

Rotation mints a new Ed25519 keypair in the HSM, bumps key_version, and kills the old key immediately. Your agent code does not need to change. The new address will differ from the old one since Cardano enterprise addresses are derived directly from the public key, so fund the new address before deactivating the old key if you have an active balance.

Try Preprod first

Get free Preprod tADA from the Cardano faucet. One request per address per 24 hours, available via web form or API. View transactions on explorer.cardano.org/preprod. Preprod runs the same protocol parameters as mainnet and is the environment we use for integration testing.

Docs: multi-chain signing keys · non-EVM transaction signing · per-chain guardrails