> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Delegated Access for EVM Wallets

> Sign messages and transactions on behalf of users with their permission using delegated access

## Overview

Delegated access lets your server perform wallet operations (signing messages, signing transactions, exporting keys) on behalf of users with their permission. This is useful for automating workflows while maintaining security through user-approved delegation.

This guide covers the server-side implementation using the Java SDK. For conceptual background, see [Delegated Access Overview](/overview/wallets/embedded-wallets/mpc/delegated-access).

## Prerequisites

* A Dynamic environment with embedded wallets (MPC) enabled
* An RSA private key registered with Dynamic for webhook decryption (PEM-encoded PKCS#8)
* [Access to delegated shares, wallet API etc. via Delegated Access](/overview/wallets/embedded-wallets/mpc/delegated-access)

## How Delegation Works

1. User approves delegation request in your application (client-side).
2. Dynamic sends an encrypted webhook to your server containing:
   * `walletId` — the user's wallet ID
   * `encryptedWalletApiKey` — wallet-scoped API key for outbound signing requests, RSA-OAEP-wrapped + AES-GCM encrypted
   * `encryptedDelegatedKeyShare` — the customer-side MPC share, encrypted with the same envelope
3. Your server decrypts both fields with your RSA private key.
4. Your server stores the credentials securely (vault).
5. Your server uses these credentials to sign on behalf of the user via `DelegatedEvmWalletClient`.

## Decrypting the Webhook Payload

Dynamic emits two encrypted blobs in each delegation webhook. Both use `RSA-OAEP-SHA256` + `AES-256-GCM` hybrid encryption. Decrypt with the RSA private key whose public counterpart is configured on your environment:

```java theme={"system"}
import xyz.dynamic.waas.delegated.DecryptedWebhookData;
import xyz.dynamic.waas.delegated.EncryptedDelegatedPayload;
import xyz.dynamic.waas.delegated.Webhook;

// Load from a secrets manager — never hardcode.
String rsaPrivateKeyPem = System.getenv("DYNAMIC_DELEGATED_RSA_PRIVATE_KEY_PEM");

EncryptedDelegatedPayload encShare  = payload.encryptedDelegatedKeyShare();
EncryptedDelegatedPayload encApiKey = payload.encryptedWalletApiKey();

DecryptedWebhookData decrypted = Webhook.decryptDelegatedWebhookData(
    rsaPrivateKeyPem, encShare, encApiKey);

// decrypted.serverKeyShare() — ServerKeyShare, the MPC server key share (sensitive)
// decrypted.walletApiKey()   — String, the per-wallet API key (sensitive)
```

`DecryptedWebhookData.toString()` is redacted — `log.debug("{}", decrypted)` won't spill key material into logs.

The envelope is `RSA-OAEP-SHA256`-wrapped `AES-256-GCM` (`alg = "HYBRID-RSA-AES-256"`). The SDK also accepts the legacy `"RSA-OAEP"` alg for backward compatibility.

## Creating the Delegated Client

```java theme={"system"}
import xyz.dynamic.waas.core.types.Environment;
import xyz.dynamic.waas.delegated.DelegatedWalletClientOpts;
import xyz.dynamic.waas.evm.delegated.DelegatedEvmWalletClient;

DelegatedWalletClientOpts opts = DelegatedWalletClientOpts.builder()
    .apiKey("your-dashboard-api-key")
    .environment(Environment.PROD)            // or Environment.PREPROD
    .environmentId("your-environment-id")
    .build();

DelegatedEvmWalletClient evm = new DelegatedEvmWalletClient(opts);
```

`DelegatedEvmWalletClient` is `AutoCloseable` — wrap it in try-with-resources or close it explicitly.

## Signing Messages

The delegated client takes the per-wallet credentials (`walletId`, `walletApiKey`, `keyShare`, `walletProperties`) plus the operation-specific inputs. **`walletProperties` is not in the webhook payload** — it's the same `WalletProperties` you persisted when the wallet was first created. If you don't have it cached, rebuild identity via `client.fetchWalletMetadata(accountAddress)` (note: that returns identity only — no `externalServerKeySharesBackupInfo`, which is fine for sign / export but not for password rotation or share recovery).

```java theme={"system"}
import xyz.dynamic.waas.evm.delegated.opts.DelegatedSignMessageOpts;

// Load WalletProperties from your own cache (Redis/Postgres), keyed by walletId.
WalletProperties walletProperties = walletCache.get(walletId);

String signature = evm.signMessage(DelegatedSignMessageOpts.builder()
    .walletId(walletId)
    .walletApiKey(decrypted.walletApiKey())
    .keyShare(decrypted.serverKeyShare())
    .walletProperties(walletProperties)
    .message("Hello, delegated".getBytes(java.nio.charset.StandardCharsets.UTF_8))
    .build()
).join();
```

## Signing Transactions

`signTransaction` returns the signed RLP hex of an EIP-1559 transaction — push it to any JSON-RPC endpoint:

```java theme={"system"}
import java.math.BigInteger;
import xyz.dynamic.waas.evm.EvmTransaction;
import xyz.dynamic.waas.evm.delegated.opts.DelegatedSignTransactionOpts;

EvmTransaction tx = EvmTransaction.builder()
    .chainId(1L)
    .nonce(BigInteger.ZERO)
    .maxFeePerGas(new BigInteger("20000000000"))
    .maxPriorityFeePerGas(new BigInteger("1000000000"))
    .gasLimit(new BigInteger("21000"))
    .to("0xRecipientAddress")
    .value(new BigInteger("100000000000000000"))
    .data("0x")
    .build();

String signedRlpHex = evm.signTransaction(DelegatedSignTransactionOpts.builder()
    .walletId(walletId)
    .walletApiKey(decrypted.walletApiKey())
    .keyShare(decrypted.serverKeyShare())
    .walletProperties(walletProperties)
    .transaction(tx)
    .build()
).join();
```

## Exporting a Private Key

```java theme={"system"}
import java.util.List;
import xyz.dynamic.waas.evm.delegated.opts.DelegatedExportOpts;

String privateKeyHex = evm.exportPrivateKey(DelegatedExportOpts.builder()
    .walletId(walletId)
    .walletApiKey(decrypted.walletApiKey())
    .externalServerKeyShares(List.of(decrypted.serverKeyShare()))
    .build()
).join();
```

<Warning>
  Exporting bypasses MPC entirely — see [Export EVM Private Key](/java/evm/export-private-key) for the same one-way-operation caveats.
</Warning>

## Revoking Delegation

When a user revokes consent (or your server detects misuse), call `revokeDelegation` on the base delegated client:

```java theme={"system"}
evm.base().revokeDelegation(walletId, decrypted.walletApiKey()).join();
```

<Warning>
  **The server endpoint for `revokeDelegation` is not yet live.** The SDK validates inputs and returns a successful future without issuing an HTTP call — so a successful `join()` does NOT mean the delegation was revoked server-side. Node v1 / Python / Go / Rust all stub this identically pending the live route. Until the endpoint ships, treat revocation as a client-side audit signal only: drop the credentials from your vault locally, and do not rely on Dynamic blocking subsequent sign calls.
</Warning>

## Security Considerations

### Credential Storage

* **Never log or expose** delegation credentials (wallet API key, key share). The SDK redacts them in `toString()` output; mirror that in your own code.
* **Store credentials encrypted at rest** in your database (envelope encryption via cloud KMS recommended — see [Storage Best Practices](/java/storage-best-practices)).
* **Use secure environment variables** for the RSA private key. Load it from AWS Secrets Manager / GCP Secret Manager / Vault, not `.env`.
* **Implement credential rotation policies** — revoke wallet API keys when no longer needed.

### Defense-in-Depth Skeleton

```java theme={"system"}
import xyz.dynamic.waas.delegated.DecryptedWebhookData;
import xyz.dynamic.waas.delegated.DelegatedWalletClientOpts;
import xyz.dynamic.waas.delegated.Webhook;
import xyz.dynamic.waas.evm.delegated.DelegatedEvmWalletClient;
import xyz.dynamic.waas.evm.delegated.opts.DelegatedSignMessageOpts;

public final class DelegatedWalletService implements AutoCloseable {
    private final String rsaPrivateKeyPem;
    private final VaultClient vault;
    private final AuditLog audit;
    private final DelegatedEvmWalletClient evm;

    public DelegatedWalletService(
            DelegatedWalletClientOpts opts,
            String rsaPrivateKeyPem,
            VaultClient vault,
            AuditLog audit
    ) {
        this.rsaPrivateKeyPem = rsaPrivateKeyPem;
        this.vault            = vault;
        this.audit            = audit;
        this.evm              = new DelegatedEvmWalletClient(opts);
    }

    public void handleWebhook(WebhookPayload payload) {
        DecryptedWebhookData decrypted = Webhook.decryptDelegatedWebhookData(
            rsaPrivateKeyPem,
            payload.encryptedDelegatedKeyShare(),
            payload.encryptedWalletApiKey()
        );

        // Vault both pieces — encrypted at rest, KMS-wrapped.
        vault.writeEnvelope("delegated:" + payload.walletId(), decrypted);
        audit.log("delegation_granted", payload.walletId());
    }

    public String signMessage(String walletId, byte[] message) {
        DelegatedCredentials creds = vault.readEnvelope("delegated:" + walletId);

        String sig = evm.signMessage(DelegatedSignMessageOpts.builder()
            .walletId(walletId)
            .walletApiKey(creds.walletApiKey())
            .keyShare(creds.serverKeyShare())
            .walletProperties(creds.walletProperties())
            .message(message)
            .build()
        ).join();

        audit.log("sign_message", walletId);
        return sig;
    }

    @Override public void close() { evm.close(); }
}
```

## Related

* [Delegated Access Overview](/overview/wallets/embedded-wallets/mpc/delegated-access) — conceptual background and flow diagram
* [Solana Delegated Access](/java/svm/delegated-access)
