AURA

dWallet Execution

How AURA uses Ika dWallet records to co-sign transactions across 6 chains without exposing a private key.

Why dWallet

Giving an AI agent a raw private key means the key can be extracted from memory, a compromised agent can drain the wallet instantly, and there is no on-chain record of what was authorized. Ika dWallet records solve all three: the agent never sees the key, every co-sign request is gated by the AURA program, and the audit trail lives on Solana.

Supported Chains

Chain IDs are u8 values used in ProposeTransactionArgs.target_chain and RegisterDwalletArgs.chain:

IDChainNotes
0Bitcoinbitcoin_manual_review_threshold_usd applies
1EthereumEVM-compatible
2SolanaNative; no bridge
3PolygonEVM-compatible
4ArbitrumEVM-compatible
5OptimismEVM-compatible

These map to the Chain enum in aura-policy: Bitcoin, Ethereum, Solana, Polygon, Arbitrum, Optimism.

Registering a dWallet

Before any proposal can be approved for a chain, a DwalletRecord must be registered against the treasury. One record per chain.

// Register an Ethereum dWallet (owner signs)
await aura.dwallet.register({
  treasury,
  chain: 1,                          // Ethereum
  dwalletId: "dwallet-abc123",       // from Ika dWallet provisioning
  address: "0xYourEthAddress",
  balanceUsd: 5_000,                 // USD cents — balance hint
});

RegisterDwalletArgs also accepts optional fields used in the full CPI path:

FieldTypeDescription
chainnumberChain ID 0–5
dwalletIdstringUnique dWallet identifier from Ika
addressstringNative address on the target chain
balanceUsdBNCurrent balance hint in USD cents
dwalletAccountPublicKey | nullIka dWallet account (for CPI path)
authorizedUserPubkeyUint8Array | nullAuthorized user public key bytes
messageMetadataDigestUint8Array | null32-byte metadata digest
publicKeyHexstring | nullRaw dWallet public key hex
timestampBNRegistration timestamp

Execution Flow

For legacy proposals with no chain binding, finalize_execution keeps the original behavior and closes the proposal once the signature is verified. For chain-bound proposals, settlement is explicit: the signed digest commits to replay-protection fields, mark_settlement_broadcast records the target-chain tx hash after relay, and confirm_settlement is the point where wallet reservations, scheduled intent counters, and policy counters are finally applied. Failed or reorged broadcasts can be resubmit_proposal with fresh replay fields or abandon_proposal to release the reservation.

Chain Binding and native_message_hash

Chain-bound proposals require native_message_hash in ProposeTransactionArgs. This is the 32-byte Keccak-256 digest of the exact bytes Ika will sign — not a digest of the AURA audit string.

native_message_hash = keccak256(exact bytes passed to Ika requestSign)

For Solana, this is keccak256(compiled_transaction_message_bytes). For EVM and Bitcoin, it is the digest of the native transaction/sighash payload the caller constructed. When native_message_hash is absent (legacy proposals), the program falls back to keccak256(canonical_audit_message_string).

The canonical chain message string also includes a bind_native_message=<hex> segment when the field is set, making the binding auditable on-chain.

Chain-bound proposal validation requires native_message_hash to be present. Solana proposals additionally require solana_recent_blockhash. The separate solana_message_hash field is a Solana-specific trace/audit field and is not required for validation.

For the TypeScript SDK helpers that drive the full approval and broadcast flow, see the dWallet Execution guide.

execute_pending Accounts

AccountSeed / SourcePurpose
operatorsignerDrives execution lifecycle
treasury["treasury", owner, agentId]Treasury PDA
message_approvalderived on dWallet programStores co-signed bytes
dwalletdWallet account from IkaThe co-signer
caller_programaura-core program IDCPI caller identity
cpi_authority["__ika_cpi_authority"] on aura-coreSigns the CPI
dwallet_program87W54kGYFQ1rgWqMeu4XTPHWXWmXSQCcjm8vCTfiq1oYIka dWallet program
dwallet_coordinatordWallet coordinator accountNetwork coordinator
external_liveness["external_liveness", treasury]Optional freshness check
system_program11111111111111111111111111111111Account creation

MessageApproval digest selection

build_message_approval_request uses native_message_hash from the proposal binding when present and falls back to keccak256(audit_message_string) for legacy unbound proposals:

Proposal typeDigest used for approve_message
Legacy (no binding)keccak256(canonical_audit_string)
Chain-boundnative_message_hash from ChainExecutionBinding

This means the MessageApproval PDA address changes depending on which digest path is taken — the PDA is derived from whichever digest was passed to approve_message.

MessageApproval PDA Derivation

The MessageApproval PDA is derived on the Ika dWallet program, not on aura-core. The SDK exports deriveMessageApprovalAddress which mirrors aura-core::find_message_approval_pda:

import { deriveMessageApprovalAddress } from "@aura-protocol/sdk-ts";

const [messageApproval] = deriveMessageApprovalAddress(
  DWALLET_DEVNET_PROGRAM_ID,
  curveCode,              // 0=secp256k1, 1=secp256r1, 2=ed25519, 3=ristretto
  publicKeyBytes,         // raw dWallet public key bytes
  signatureSchemeCode,    // Ika signature scheme code
  messageDigest,          // 32-byte Keccak-256 digest
  messageMetadataDigest,  // optional 32-byte metadata digest
);

Seeds (in order):

  1. b"dwallet"
  2. u16_le(curveCode) ++ publicKey — chunked into 32-byte segments
  3. b"message_approval"
  4. u16_le(signatureSchemeCode)
  5. messageDigest (32 bytes)
  6. messageMetadataDigest (32 bytes, only if non-zero)

Bitcoin Manual Review

Bitcoin transactions above bitcoin_manual_review_threshold_usd (default: 5_000 USD cents = $50) are blocked by ViolationCode::BitcoinManualReview in evaluate_public_precheck. A guardian must approve via approve_pending_execution before execute_pending can proceed.

finalize_execution Accounts

AccountPurpose
operatorSigner driving finalization
treasuryTreasury PDA
message_approvalMessageApproval PDA with co-signed bytes
swarm_poolOptional — updated after finalization if swarm is configured
budget_envelopeOptional — updated after finalization if envelope is configured
exposure_groupOptional — updated after finalization if group is configured
external_livenessOptional — freshness check if liveness_config requires it

confirm_settlement Accounts

AccountPurpose
operatorOwner or AI authority confirming the target-chain settlement
treasuryTreasury PDA containing the signed pending proposal
swarm_poolOptional - updated when the proposal consumes shared swarm budget
budget_envelopeOptional - updated when the proposal consumes a scoped budget
exposure_groupOptional - updated when the proposal counts against a cross-treasury exposure group
dwallet_stateOptional - required when the proposal reserved a chain-native asset transfer
scheduled_intentOptional - settled when the signed proposal was promoted from a scheduled intent

ChainProfileAccount

Custom chain codes require a ChainProfileAccount at [b"chain_profile", chain_code]. The profile stores address format, replay scheme, finality model, curve/scheme, native gas asset, optional EVM chain id, and required confirmation depth. Built-in chains have defaults, but a profile may still be supplied to override settlement or replay policy.

Security Properties

PropertyGuarantee
Key never exposedAgent receives signed bytes, not the private key
Authorization gatedOnly aura-core's cpi_authority PDA can request co-signs
Per-chain isolationEach chain has its own DwalletRecord; chain-bound proposals also bind native replay fields
Settlement gateChain-bound proposals are not counted as settled until confirm_settlement reaches the required confirmations
Failure handlingReorged or failed broadcasts can be re-signed with fresh replay data or abandoned with reservations released
Audit trailEvery execute_pending and finalize_execution emits on-chain events
RevocableOwner can deregister a dWallet record at any time
Liveness checksLivenessConfig.require_dwallet_freshness can require a fresh ExternalLiveness record

On this page