lucidAGENTS
Packages

@lucid-agents/wallet

Configure signing wallets for identity, authentication, and outbound transactions.

@lucid-agents/wallet gives a Lucid runtime a consistent interface to local, thirdweb, custom viem, and Lucid-hosted signing wallets. Use it when an agent must sign an identity challenge, authenticate to another service, or submit an outbound transaction.

It does not configure the address that receives x402 payments. A seller's receiving address belongs in the payments configuration.

Install

bun add @lucid-agents/wallet viem

Install thirdweb as well only when you use the thirdweb connector:

bun add thirdweb

Add wallets to a runtime

import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { wallets, walletsFromEnv } from '@lucid-agents/wallet';

const runtime = await createAgent({
  name: 'identity-agent',
  version: '1.0.0',
})
  .use(wallets({ config: walletsFromEnv() }))
  .use(http())
  .build();

wallets() is optional and accepts { config?: WalletsConfig }. When neither an agent nor developer wallet is configured, runtime.wallets is undefined.

Choose a wallet role

RolePurposeSupported configurations
agentSigns agent challenges and can expose a transaction-ready wallet clientlocal, signer, thirdweb, lucid
developerOptional operator-controlled signerlocal, signer

The developer role is still a signing wallet. An address without a private key does not create a developer wallet.

Configure from environment

For a local agent wallet:

AGENT_WALLET_TYPE=local
AGENT_WALLET_PRIVATE_KEY=0x...
AGENT_WALLET_CAIP2=eip155:84532
AGENT_WALLET_RPC_URL=https://your-base-sepolia-rpc.example
AGENT_WALLET_CHAIN_ID=84532
AGENT_WALLET_CHAIN_NAME="Base Sepolia"

Then load it:

import { walletsFromEnv } from '@lucid-agents/wallet';

const config = walletsFromEnv();

walletsFromEnv(overrides?, env?) reads environment values, then applies explicit overrides per role. It returns undefined when neither role is configured.

Agent wallet variables

VariableRequiredMeaning
AGENT_WALLET_TYPEYes for an agent walletlocal, thirdweb, or lucid
AGENT_WALLET_PRIVATE_KEYFor local0x-prefixed private key
AGENT_WALLET_SECRET_KEYFor thirdwebthirdweb secret key
AGENT_WALLET_CLIENT_IDNothirdweb client ID
AGENT_WALLET_LABELNothirdweb server-wallet label; defaults to agent-wallet
AGENT_WALLET_CHAIN_IDFor thirdweb; optional metadata for localNumeric EVM chain ID; thirdweb defaults to 84532
AGENT_WALLET_BASE_URLFor lucidLucid wallet API base URL; LUCID_BASE_URL and LUCID_API_URL are fallbacks
AGENT_WALLET_AGENT_REFFor lucidAgent identifier used by the wallet service
AGENT_WALLET_ACCESS_TOKENNoInitial bearer token for a Lucid-hosted wallet
AGENT_WALLET_HEADERSNoJSON object of additional request headers
AGENT_WALLET_AUTHORIZATION_CONTEXTNoJSON object merged into signing authorization context

Local wallets also accept metadata through AGENT_WALLET_ADDRESS, AGENT_WALLET_CAIP2, AGENT_WALLET_CHAIN, AGENT_WALLET_CHAIN_TYPE, AGENT_WALLET_PROVIDER, and AGENT_WALLET_LABEL. A local wallet client can use AGENT_WALLET_RPC_URL, AGENT_WALLET_CHAIN_ID, and AGENT_WALLET_CHAIN_NAME.

Developer wallet variables

Set DEVELOPER_WALLET_PRIVATE_KEY to create the developer wallet. The same metadata and wallet-client suffixes used by local agent wallets are available under the DEVELOPER_WALLET_ prefix.

DEVELOPER_WALLET_ADDRESS by itself is metadata only and does not create a wallet.

Configure in code

Local private key

import type { WalletsConfig } from '@lucid-agents/types/wallets';

const config: WalletsConfig = {
  agent: {
    type: 'local',
    privateKey: process.env.AGENT_WALLET_PRIVATE_KEY!,
    caip2: 'eip155:84532',
    walletClient: {
      rpcUrl: process.env.BASE_SEPOLIA_RPC_URL,
      chainId: 84532,
      chainName: 'Base Sepolia',
    },
  },
};

Without a wallet-client RPC configuration, local development falls back to http://localhost:8545 and chain ID 31337. Supply an explicit chain ID when using a custom RPC URL.

Existing viem wallet client

import { createWalletClient, http as viemHttp } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';

const walletClient = createWalletClient({
  account: privateKeyToAccount(process.env.AGENT_WALLET_PRIVATE_KEY!),
  chain: baseSepolia,
  transport: viemHttp(process.env.BASE_SEPOLIA_RPC_URL),
});

const config = {
  agent: {
    type: 'signer' as const,
    walletClient,
  },
};

The configuration property is walletClient, not signer. Programmatic signer wallets are not selected through AGENT_WALLET_TYPE.

thirdweb server wallet

const config = {
  agent: {
    type: 'thirdweb' as const,
    secretKey: process.env.THIRDWEB_SECRET_KEY!,
    clientId: process.env.THIRDWEB_CLIENT_ID,
    walletLabel: 'production-agent',
    chainId: 8453,
  },
};

The connector creates or resolves a thirdweb Engine server wallet lazily on first use and adapts it to a viem wallet client.

Lucid-hosted wallet

const config = {
  agent: {
    type: 'lucid' as const,
    baseUrl: 'https://wallets.example.com',
    agentRef: 'agent_123',
    accessToken: process.env.LUCID_WALLET_TOKEN,
  },
};

The hosted connector signs challenges over HTTP. It does not expose a local signer or wallet client. If a token is issued after startup, update it with runtime.wallets?.agent?.setAccessToken?.(token) before signing.

Runtime API

type WalletsRuntime =
  | {
      agent?: {
        kind: 'local' | 'signer' | 'lucid' | 'thirdweb';
        connector: WalletConnector;
        setAccessToken?(token: string | null): void;
      };
      developer?: {
        kind: 'local' | 'signer';
        connector: WalletConnector;
      };
    }
  | undefined;

Handles do not cache an address property. Resolve current metadata or an address through the connector:

const connector = runtime.wallets?.agent?.connector;
const address = await connector?.getAddress?.();
const metadata = await connector?.getWalletMetadata();

Every connector implements challenge signing and metadata lookup:

interface WalletConnector {
  signChallenge(challenge: AgentChallenge): Promise<string>;
  getWalletMetadata(): Promise<WalletMetadata | null>;
  supportsCaip2?(caip2: string): boolean | Promise<boolean>;
  getAddress?(): Promise<string | null>;
  getCapabilities?(): WalletCapabilities | null | undefined;
  getSigner?(): Promise<LocalEoaSigner | null>;
  getWalletClient?<TClient = unknown>(): Promise<TClient | null>;
}

The shared contracts are imported from @lucid-agents/types/wallets; the wallet package does not re-export every shared type.

Sign and transact

Challenge signing is a connector method:

const signature =
  await runtime.wallets?.agent?.connector.signChallenge(challenge);

There are no package-level signChallenge() or verifyChallenge() helpers.

For a plain message, first request the optional signer:

const signer = await runtime.wallets?.agent?.connector.getSigner?.();
if (!signer) throw new Error('This connector does not expose a local signer');

const signature = await signer.signMessage('Lucid authorization');

For contract calls, request the wallet client directly. The method returns the client itself, not an object containing { client }:

import type { WalletClient } from 'viem';

const walletClient =
  await runtime.wallets?.agent?.connector.getWalletClient?.<WalletClient>();

if (!walletClient?.account) {
  throw new Error('A transaction-ready wallet client is unavailable');
}

const hash = await walletClient.sendTransaction({
  account: walletClient.account,
  chain: walletClient.chain,
  to: '0x0000000000000000000000000000000000000000',
  value: 0n,
});

Check getCapabilities() when code must support several connector kinds. A capability is a hint; still handle a null optional result.

Standalone factories and exports

Use createAgentWallet, createDeveloperWallet, or createWalletsRuntime outside the extension builder. The package also exports:

  • LocalEoaWalletConnector, ViemWalletConnector, ThirdwebWalletConnector, and ServerOrchestratorWalletConnector
  • createPrivateKeySigner and createSignerConnector
  • challenge normalization/extraction helpers
  • viem signature helpers signMessageWithViem and signTypedDataWithViem

Security and production checks

  • Do not put private keys in source, client bundles, logs, or checked-in .env files. Prefer a secret manager or a remote signer in production.
  • Treat hosted-wallet access tokens as signing authority. Scope, rotate, and revoke them like private credentials.
  • Validate the requested CAIP-2 network before signing or sending a transaction. Wallet capability does not prove the caller intended that network.
  • Bind authorization challenges to a short expiry, nonce, audience, and the exact action being authorized. The wallet package signs the challenge; your verifier owns replay prevention and policy.
  • Separate seller payment addresses from operational signing keys unless your custody model explicitly requires them to be the same.

Troubleshooting

SymptomCauseFix
AGENT_WALLET_TYPE ... is requiredAgent wallet variables were supplied without a typeSet local, thirdweb, or lucid
AGENT_WALLET_PRIVATE_KEY ... is requiredA local agent wallet has no keyInject a 0x-prefixed key through secrets management
chainId must be explicitly providedA custom RPC has no chain identitySet wallet-client chainId or a numeric CAIP-2 value
Hosted signing reports a missing bearer tokenaccessToken was absent or expiredSet it in config or call setAccessToken()
getWalletClient() returns nullThe connector does not expose transactionsUse a local, signer, or thirdweb connector, or branch on capabilities
thirdweb import failsOptional peer dependency is missingInstall a compatible thirdweb 5.x release

For the identity flow that consumes an agent wallet, continue to ERC-8004 identity.

On this page