If you run an XRPL agent, the standard setup is to load a wallet from a family seed stored in an environment variable:
const wallet = Wallet.fromSeed(process.env.XRPL_SEED!);That seed is in memory from the moment the process starts. It stays there the entire time the agent is running. Derive a private key from it, sign with it, lose it to a prompt injection, a bad log line, a serialization bug. And unlike a compromised API credential, you cannot rotate an XRPL seed after the fact if someone already has it.
We shipped XRPL on the Intents API so agents stop holding seeds or keys entirely. The agent sends a chain, a recipient, and an amount. The server builds the transaction, signs it in the HSM, and broadcasts. Back comes a tx_hash, not a key. For anything beyond a simple payment, there is xrpl_tx_json to pass the full transaction and have it signed the same way.
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": "xrp" }'You get back an Ed25519 public key and a Base58Check r-address. The EDprefix on the public key identifies it as Ed25519 rather than the older secp256k1 default, which is XRPL's own convention for distinguishing the two. The private key goes into the org's __agent-keys vault, encrypted under Cloud KMS. The API never returns it.
{
"chain": "xrp",
"curve": "Ed25519",
"address": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"public_key": "ED...",
"key_version": 1
}Sending XRP and everything else
Signing runs through xrpl-rust using its binary codec. The server auto-fills Account, Fee (12 drops by default), Sequence, LastLedgerSequence, Flags, and SigningPubKey before signing. You do not manage sequence numbers or ledger windows.
For a payment:
curl -X POST "https://api.1claw.xyz/v1/agents/$AGENT_ID/transactions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"chain": "xrp-testnet",
"to": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
"value": "10",
"destination_tag": 12345
}'value is plain XRP as a decimal string. "10" is 10,000,000 drops. The destination tag is included in the signed transaction body and appears in the audit log.
XRPL supports 30 more transaction types beyond basic payments (31 total). OfferCreate and OfferCancel for the native DEX, AMMCreate through AMMVote for liquidity pools, NFTokenMint through NFTokenCancelOffer for XLS-20 NFTs, EscrowCreate through EscrowCancel, payment channels, checks, TrustSet, SignerListSet, AccountDelete. All of them are signed the same way: pass xrpl_tx_json with the transaction body and the server handles the rest.
# TrustSet
curl -X POST "https://api.1claw.xyz/v1/agents/$AGENT_ID/transactions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"chain": "xrp-testnet",
"xrpl_tx_json": {
"TransactionType": "TrustSet",
"LimitAmount": {
"currency": "USD",
"issuer": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
"value": "1000"
}
}
}'# EscrowCreate
curl -X POST "https://api.1claw.xyz/v1/agents/$AGENT_ID/transactions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"chain": "xrp-testnet",
"xrpl_tx_json": {
"TransactionType": "EscrowCreate",
"Amount": "10000000",
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
"FinishAfter": 741494400
}
}'Chains: xrp and xrp-testnet. No RPC configuration needed. The server connects via JSON-RPC to xrplcluster.com for mainnet and s.altnet.rippletest.net:51234 for testnet.
Sequence numbers are fetched from the account's on-chain state before every submit and incremented atomically. LastLedgerSequence is set to current_ledger + 20. If a transaction is not included in the next 20 ledgers it becomes permanently invalid with no manual cleanup required.
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 back and submit it yourself via whatever XRPL node you prefer.
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. Guardrails cap what a compromised agent can actually do on-chain. Every XRPL submit runs through guardrail checks in the handler before the HSM unwraps anything. Configure them once on the agent via dashboard, SDK, or CLI.
- Allowed chains (
tx_allowed_chains): lock an agent toxrp-testnetduring dev, orxrponly in prod - Destination allowlist (
tx_to_allowlist): only send to r-addresses you have pre-approved. Anything else returns 403 at signing time - Per-transaction cap (
tx_max_value): max XRP per transaction as a decimal string."100"= 100 XRP - Daily spend cap (
tx_daily_limit): rolling 24-hour limit on XRP-family spend. The server trackstx_spent_today_by_chain.xrp, not a cross-chain total - XRPL transaction type allowlist (
xrpl_allowed_tx_types): restrict the agent to a specific subset of the 31 supported types. A payment agent has no business signing aSignerListSetorAccountDelete
That last one is specific to XRPL and worth understanding. An agent with full signing authority over an XRPL account can do significant damage beyond just draining the balance: delete the account, reconfigure multisig, drain trust lines. Four dangerous transaction types — SetRegularKey, SignerListSet, AccountSet, and AccountDelete— are blocked by default even when the allowlist is empty. You have to explicitly add them to xrpl_allowed_tx_types to unlock them. For everything else, setting the allowlist to ["Payment"] locks the agent to payments only. Anything outside the list returns 403 before the server touches the key.
Example: a DEX trading agent on testnet locked to offers only:
await client.agents.update(agentId, {
intents_api_enabled: true,
tx_allowed_chains: ["xrp-testnet"],
xrpl_allowed_tx_types: ["OfferCreate", "OfferCancel"],
per_chain_guardrails: {
xrp: {
to_allowlist: ["rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"],
max_value: "500",
daily_limit: "2000",
},
},
});Prompt injection cannot talk its way around this. The agent submits an intent; the server checks the type allowlist, destination allowlist, and caps; only then does signing happen. If anything fails, you get 403 and nothing hits the ledger.
Rotate and revoke
| Operation | Endpoint |
|---|---|
| Provision | POST /v1/agents/{id}/signing-keys |
| List | GET /v1/agents/{id}/signing-keys |
| Rotate | POST /v1/agents/{id}/signing-keys/xrp/rotate |
| Deactivate | DELETE /v1/agents/{id}/signing-keys/xrp |
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.
One note on XRPL key rotation: if you use the SetRegularKey pattern on-chain (a separate operational key that signs on behalf of the master), rotating the 1Claw key and updating the on-chain regular key record keeps your master key fully cold. The 1Claw rotation endpoint handles the HSM side; the on-ledger SetRegularKey transaction is a separate step you control.
Try XRPL Testnet first
Get free testnet XRP from the XRPL faucet. The faucet issues 1,000 testnet XRP instantly with no login. View transactions on testnet.xrpl.org. Accounts go live as soon as they receive funds and meet the 10 XRP reserve.
Docs: multi-chain signing keys · non-EVM transaction signing · XRPL transaction types