
Vechain Kit
- 87 installs
- 9 repo stars
- Updated June 11, 2026
- vechain/vechain-ai-skills
Helps with ai & agent building tasks.
About
vechain-kit is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- vechain-kit
- AI & Agent Building
- AI-coding skill
Vechain Kit by the numbers
- 87 all-time installs (skills.sh)
- +3 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #4,982 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vechain/vechain-ai-skills --skill vechain-kitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 87 |
|---|---|
| repo stars | ★ 9 |
| Last updated | June 11, 2026 |
| Repository | vechain/vechain-ai-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
VeChain Kit Skill
CRITICAL RULES
1. Read reference files FIRST. When the user's request involves any topic in the reference table below, read those files before doing anything else — before writing code, before making decisions. Briefly mention which files you are reading so the user can confirm the skill is active (e.g., "Reading VeChain Kit reference..."). 2. Information priority for VeChain topics: (a) Reference files in this skill — always the primary source. (b) VeChain MCP tools — use @vechain/mcp-server for on-chain data, transaction building, and live network queries; use Kapa AI MCP for VeChain documentation lookups. (c) Web search — only as a last resort, and only for topics NOT covered in the reference files. 3. Prefer working directly in the main conversation for VeChain tasks. Plan mode and subagents do not inherit skill context and may fall back to web search instead of using reference files. 4. After compaction or context loss, re-read this SKILL.md to restore awareness of the reference table and operating procedure before continuing work.
Scope
Use this Skill for the VeChain Kit and dapp-kit packages specifically:
- VeChain Kit: installation, setup, configuration, Privy integration
- dapp-kit: lightweight wallet connection for non-React or minimal setups
- Wallet connection, social login (email, Google, passkey), smart accounts
- Pre-built UI components (WalletButton, TransactionModal)
- Hooks (useWallet, useSendTransaction, useCallClause, token/domain/oracle hooks)
- Theming and Privy setup
- i18n with react-i18next: bi-directional language sync (Kit ↔ host app), pre-commit/ESLint for missing or unused translation keys
For generic frontend patterns (React Query, Turborepo, state management, Chakra UI, transaction UX), see the frontend skill.
Default stack
| Layer | Default | Alternative |
|---|---|---|
| Frontend | @vechain/vechain-kit | @vechain/dapp-kit-react (lightweight/non-React) |
| Node | Node 20 LTS (managed via nvm) | -- |
Operating procedure
1. Check Node version
Before installing dependencies or running any command:
- Check if
.nvmrcexists in the project root. If yes, runnvm use. - If
.nvmrcdoes not exist, create one with20(Node 20 LTS) and runnvm use.
2. Detect project structure
turbo.jsonpresent → follow Turborepo conventions (apps/frontend,packages/*)- Use
useThorfor Thor client access (both VeChain Kit and dapp-kit v2).useConnexis deprecated everywhere. - Apply conditional patterns (Chakra UI, i18n, Zustand) only when the project uses them
3. Choose the right library
When to ask the user: If the project doesn't already use VeChain Kit or dapp-kit and the user hasn't specified which to use, ask before choosing. Key questions:
- Do you need social login (email, Google, passkey)? → VeChain Kit
- Do you want pre-built UI modals and hooks (WalletButton, TransactionModal, token hooks)? → VeChain Kit
- Do you want a lightweight wallet-only integration with minimal dependencies? → dapp-kit
- Non-React framework? → dapp-kit
4. Clarify before implementing
When the user's request is ambiguous or could be solved multiple ways, ask before building. Separate research from implementation.
5. Implement with VeChain-specific correctness
- Network: always explicit (
mainnet/testnet/solo) - Social login: Generic Delegator auto-enabled (users pay gas in VET/VTHO/B3TR); app-sponsored delegation optional for better UX; smart accounts; pre-fetch data before
sendTransaction
6. Verify and deliver
A task is not complete until all applicable gates pass:
1. Code compiles — no build errors (npm run build or equivalent succeeds) 2. Tests pass — existing tests still pass; new logic has test coverage 3. Risk notes documented — any signing, fee, or token-transfer implications are called out
Reference files
Read the matching files BEFORE doing anything else. See Critical Rules above.
| Topic | File | Read when user mentions... |
|---|---|---|
| Setup & config | references/kit-setup.md | Installing VeChain Kit, provider setup, CSS framework, Tailwind, env vars, login methods, legal documents, ecosystem apps, common pitfalls |
| Hooks | references/kit-hooks.md | useWallet, useCallClause, useSendTransaction, useBuildTransaction, useSignMessage, contract reads, transactions, VET domains, NFTs, blockchain hooks, language/currency, @vechain/contract-getters |
| Components & modals | references/kit-components.md | WalletButton, TransactionModal, TransactionToast, modal hooks, isolated views |
| Social login | references/kit-social-login.md | Social login, smart accounts, account abstraction, Privy setup, fee delegation for social login, DIY social login |
| Theming | references/kit-theming.md | Theming, colors, fonts, buttons, glass effects, bottom sheet, Chakra UI compatibility, webpack fallbacks |
| dapp-kit | references/frontend-dappkit.md | dapp-kit, DAppKitProvider, lightweight wallet |
| Translations + Kit | references/translations-vechain-kit.md | i18n, translations, language sync, VeChain Kit language, missing translations, pre-commit, ESLint, unused keys |
dapp-kit (Lightweight Alternative)
When to use
Use when the user asks about: dapp-kit, DAppKitProvider, lightweight wallet connection, non-React VeChain frontend.
See the frontend skill for choosing VeChain Kit vs dapp-kit and shared frontend patterns.
When to Choose dapp-kit Over VeChain Kit
- Bundle size is critical
- Non-React framework (Vue, Svelte, Angular)
- Wallet connection only (no social login, no pre-built transaction UI)
- Minimal dependency footprint
---
Setup
npm install @vechain/dapp-kit-reactimport { DAppKitProvider } from '@vechain/dapp-kit-react';
<DAppKitProvider
nodeUrl="https://testnet.vechain.org/"
genesis="test"
usePersistence={true}
allowedWallets={['veworld', 'wallet-connect']}
>
<YourApp />
</DAppKitProvider>---
Available Hooks
| Hook | Description |
|---|---|
useWallet() | Connection state, account address, disconnect |
useThor() | Thor client for direct blockchain access |
useWalletModal() | Open/close wallet connection modal |
useVechainDomain() | Resolve .vet domain names |
useSendTransaction() | Send transactions with optional fee delegation |
useThor
import { useThor } from '@vechain/dapp-kit-react';
const thor = useThor();
// Use thor for contract reads, block queries, etc.useSendTransaction
import { useSendTransaction } from '@vechain/dapp-kit-react';
function SendButton() {
const { sendTransaction } = useSendTransaction();
const handleSend = async () => {
const result = await sendTransaction({
clauses: [
{ to: '0x...', value: '0x0', data: encodedCallData },
],
comment: 'Description for the user',
// Optional: app-sponsored fee delegation
delegatorUrl: 'https://sponsor-testnet.vechain.energy/by/YOUR_PROJECT_ID',
});
console.log('Transaction ID:', result.id);
};
return <button onClick={handleSend}>Send</button>;
}Components
import { WalletButton } from '@vechain/dapp-kit-react';
<WalletButton />---
Limitations vs VeChain Kit
- No social login (Privy) -- DIY only, see the vechain-kit skill (
references/kit-social-login.md) - No pre-built transaction UI (TransactionModal, TransactionToast)
- No contract read hooks (useCallClause) -- build your own with React Query +
useThor() - No token management hooks
- No smart account support
- No i18n
For contract reads without VeChain Kit's useCallClause, build custom React Query hooks:
import { useQuery } from '@tanstack/react-query';
import { useThor } from '@vechain/dapp-kit-react';
export function useTokenBalance(contractAddress: string, userAddress: string) {
const thor = useThor();
return useQuery({
queryKey: ['contract', contractAddress, 'balanceOf', userAddress],
queryFn: async () => {
const contract = thor.contracts.load(contractAddress, ERC20_ABI);
return contract.read.balanceOf(userAddress);
},
enabled: !!userAddress && !!thor,
staleTime: 10_000,
});
}VeChain Kit — Components & Modals
When to use
Use when the user asks about: WalletButton, TransactionModal, TransactionToast, modal hooks, isolated views, or VeChain Kit UI components.
---
WalletButton
Acts as login button when disconnected and account button when connected.
import { WalletButton } from '@vechain/vechain-kit';
<WalletButton mobileVariant="icon" desktopVariant="iconAndDomain" />
// Custom styling via buttonStyle (Chakra UI style props)
<WalletButton
mobileVariant="iconDomainAndAssets"
desktopVariant="iconDomainAndAssets"
buttonStyle={{
background: '#f08098',
color: 'white',
border: '2px solid #000',
_hover: { background: '#db607a' },
}}
/>Variants: icon | iconAndDomain | iconDomainAndAddress | iconDomainAndAssets
Note: some variants adapt based on available data (e.g. iconDomainAndAssets only shows assets if the user has any).
TransactionModal
import { TransactionModal, useTransactionModal } from '@vechain/vechain-kit';
const { open, close, isOpen } = useTransactionModal();
<TransactionModal
isOpen={isOpen}
onClose={close}
status={status}
txReceipt={txReceipt}
txError={error}
onTryAgain={handleTryAgain}
uiConfig={{
title: 'Confirm Transaction',
description: 'Sending tokens...',
showShareOnSocials: true,
showExplorerButton: true,
isClosable: true,
}}
/>Modal Hooks
All modal hooks return { open, close, isOpen }. Pass { isolatedView: true } to open() to prevent the user from navigating to other Kit sections.
import {
useAccountModal, useProfileModal, useSendTokenModal,
useReceiveModal, useConnectModal, useDAppKitWalletModal,
useAccountCustomizationModal, useAccessAndSecurityModal,
useChooseNameModal, useUpgradeSmartAccountModal,
useWalletModal, useTransactionToast,
useExploreEcosystemModal, useNotificationsModal, useFAQModal,
} from '@vechain/vechain-kit';
const { open: openProfile } = useProfileModal();
openProfile({ isolatedView: true }); // Prevent navigation to other kit sections
// Wallet-only connection (bypasses social login)
const { open: openWalletModal } = useDAppKitWalletModal();Account: useAccountModal, useProfileModal, useAccountCustomizationModal, useAccessAndSecurityModal, useChooseNameModal, useUpgradeSmartAccountModal Wallet/Connection: useConnectModal, useWalletModal, useDAppKitWalletModal Transaction: useTransactionModal, useTransactionToast, useSendTokenModal, useReceiveModal Features: useExploreEcosystemModal, useNotificationsModal, useFAQModal
VeWorld mobile: When the app is accessed from VeWorld's in-app browser, VeWorld is automatically enforced as the primary authentication method.
VeChain Kit — Hooks
When to use
Use when the user asks about: useWallet, useCallClause, useSendTransaction, useBuildTransaction, useSignMessage, useSignTypedData, contract reads, transactions, VET domains, NFTs, blockchain hooks, language/currency hooks, or @vechain/contract-getters.
---
General
All hooks use TanStack Query (React Query) and return a consistent shape:
{ data, isLoading, isError, error, refetch, isRefetching }All Kit queries use the VECHAIN_KIT prefix — use it for broad invalidation:
queryClient.invalidateQueries({ queryKey: ['VECHAIN_KIT'] }); // all Kit queries
queryClient.invalidateQueries({ queryKey: ['VECHAIN_KIT', 'CURRENT_BLOCK'] }); // specificSee the frontend skill for React Query caching, invalidation, and loading state patterns.
useWallet -- Connection State
import { useWallet } from '@vechain/vechain-kit';
function MyComponent() {
const {
account, // Active account { address, domain, image } — smart account for Privy, wallet for DappKit
connectedWallet, // Current wallet regardless of method (Privy embedded or self-custody)
smartAccount, // { address, domain, image, isDeployed, isActive, version }
privyUser, // Privy User object if connected via Privy, null otherwise
connection, // Connection state and metadata
disconnect, // Disconnects + dispatches 'wallet_disconnected' event
} = useWallet();
// connection properties:
// isConnected, isLoading,
// isConnectedWithSocialLogin, isConnectedWithDappKit,
// isConnectedWithCrossApp, isConnectedWithPrivy, isConnectedWithVeChain,
// isInAppBrowser (true when running in VeWorld mobile browser),
// source: { type: 'privy' | 'wallet' | 'privy-cross-app', displayName },
// nodeUrl, delegatorUrl, chainId, network
if (!connection.isConnected) return <div>Not connected</div>;
return <div>Connected: {account?.address}</div>;
}SmartAccount: isDeployed indicates whether the smart account contract is deployed on-chain (deployed lazily on first transaction to save gas). version is the contract version (V3 required for multi-clause + replay protection).
useCallClause -- Contract Reads (preferred pattern)
Use useCallClause for all contract read operations. It wraps React Query for caching, refetching, and loading states. Prefer typed contract factories from @vechain/vechain-contract-types or your own TypeChain output.
import { useCallClause, getCallClauseQueryKey } from '@vechain/vechain-kit';
import { MyContract__factory } from '../typechain-types';
// Basic usage with typed factory ABI
export const useTokenBalance = (address: string) => {
return useCallClause({
abi: MyContract__factory.abi,
address: CONTRACT_ADDRESS,
method: 'balanceOf',
args: [address],
queryOptions: { enabled: !!address },
});
};
// In a component
function Balance({ address }: { address: string }) {
const { data, isLoading } = useTokenBalance(address);
if (isLoading) return <Skeleton height="20px" width="100px" />;
return <div>Balance: {data?.toString()}</div>;
}Data transformation with select (preferred over useMemo in components):
return useCallClause({
abi: VOT3__factory.abi,
address: contractAddress,
method: 'convertedB3trOf' as const,
args: [address ?? ''],
queryOptions: {
enabled: !!address,
select: (data) => ({
balance: ethers.formatEther(data[0]),
formatted: humanNumber(ethers.formatEther(data[0])),
}),
},
});Query keys for cache invalidation:
import {
getCallClauseQueryKey,
getCallClauseQueryKeyWithArgs,
} from '@vechain/vechain-kit';
// Without args (for methods with no params)
const key = getCallClauseQueryKey({
abi, address: contractAddress, method: 'currentRoundId' as const,
});
// With args (for methods with params)
const key = getCallClauseQueryKeyWithArgs({
abi, address: contractAddress, method: 'balanceOf' as const, args: [address],
});
queryClient.invalidateQueries({ queryKey: key });Organize contract hooks in a dedicated directory (e.g., src/api/contracts/):
src/api/contracts/
├── useTokenBalance.ts
├── useTokenAllowance.ts
├── useVaultDeposit.ts
└── index.tsBatch Contract Reads
Use executeMultipleClausesCall for multiple reads in one call:
import { executeMultipleClausesCall } from '@vechain/vechain-kit';
const thor = useThor();
const results = await executeMultipleClausesCall({
thor,
calls: addresses.map((addr) => ({
abi: ERC20__factory.abi,
functionName: 'balanceOf',
address: addr as `0x${string}`,
args: [userAddress],
})),
});useBuildTransaction -- Clause Builder Pattern
Wraps useSendTransaction with a clause-builder function. Use thor.contracts.load().clause to build clauses from loaded contracts:
import { useBuildTransaction, useWallet } from '@vechain/vechain-kit';
const useApproveAndSwap = () => {
const { account } = useWallet();
const thor = useThor();
return useBuildTransaction({
clauseBuilder: (tokenAddress: string, amount: string) => {
if (!account?.address) return [];
return [
{
...thor.contracts.load(tokenAddress, ERC20__factory.abi)
.clause.approve(swapAddress, ethers.parseEther(amount)).clause,
comment: 'Approve token spending',
},
{
...thor.contracts.load(swapAddress, SwapContract__factory.abi)
.clause.swap(tokenAddress, ethers.parseEther(amount)).clause,
comment: 'Execute swap',
},
];
},
onTxConfirmed: () => {
queryClient.invalidateQueries({ queryKey: ['TOKEN_BALANCE'] });
},
});
};useSendTransaction -- Core Transaction Hook
Use this for all transactions. Handles both wallet and social login users automatically.
import { useSendTransaction, useWallet } from '@vechain/vechain-kit';
import { useQueryClient } from '@tanstack/react-query';
function TransactionComponent() {
const { account } = useWallet();
const queryClient = useQueryClient();
const {
sendTransaction,
status, // 'ready' | 'pending' | 'waitingConfirmation' | 'success' | 'error'
txReceipt,
resetStatus,
isTransactionPending,
error, // { type: 'UserRejectedError' | 'RevertReasonError', reason }
} = useSendTransaction({
signerAccountAddress: account?.address ?? '',
// Gas options (pick one):
// gasPadding: 0.2, // Float 0–1: adds % buffer on top of estimated gas
// suggestedMaxGas: 40000000, // Integer: explicit gas cap, overrides estimation + padding
onTxConfirmed: () => {
// CRITICAL: Invalidate ALL queries affected by this transaction.
// Think through every component that reads data changed by the tx
// (balances, registration status, navbar items, banners, lists).
// See frontend.md "Cache Invalidation After Transactions" for details.
queryClient.invalidateQueries({
queryKey: getCallClauseQueryKey(CONTRACT, 'balanceOf', [account?.address]),
});
},
});
const handleSend = async () => {
await sendTransaction([
{
to: '0xContractAddress',
value: '0x0',
data: '0xencodedFunctionData',
comment: 'User-facing description of this operation',
abi: functionABI, // Optional: for UI display
},
]);
};
return (
<button onClick={handleSend} disabled={isTransactionPending}>
{status === 'pending' ? 'Sending...' : 'Send Transaction'}
</button>
);
}Critical: useSendTransaction is mandatory when social login is enabled. For apps without social login, you can alternatively use the signer exported by the kit and follow the SDK transaction guides directly.
Critical: Pre-fetch all data before calling sendTransaction. Fetching during submission can trigger browser pop-up blockers for social login users.
Retry pattern: Use resetStatus + onTryAgain for retry UX:
const handleTryAgain = useCallback(async () => {
resetStatus();
await sendTransaction(clauses);
}, [sendTransaction, clauses, resetStatus]);
<TransactionModal onTryAgain={handleTryAgain} isClosable /* ...other props */ />Per-transaction delegation: Override fee delegation for specific transactions:
// App sponsors this transaction
await sendTransaction(clauses, 'https://your-delegator.com/delegate');
// User pays via Generic Delegator (default)
await sendTransaction(clauses);useTransferVET / useTransferERC20 -- Convenience Hooks
import { useTransferVET, useTransferERC20, useWallet } from '@vechain/vechain-kit';
// VET transfer
const { sendTransaction } = useTransferVET({
senderAddress: account?.address ?? '',
receiverAddress: '0xRecipient',
amount: '1000000000000000000', // 1 VET in wei
});
// ERC-20 transfer
const { sendTransaction } = useTransferERC20({
senderAddress: account?.address ?? '',
receiverAddress: '0xRecipient',
amount: '1000000000000000000',
tokenAddress: '0xTokenContract',
tokenName: 'B3TR',
});Multi-Clause Transactions
const handleBatchOperation = async () => {
await sendTransaction([
{ to: tokenAddr, value: '0x0', data: approveData, comment: 'Approve spending' },
{ to: vaultAddr, value: '0x0', data: depositData, comment: 'Deposit tokens' },
]);
};Login Hooks
import {
useLoginWithPasskey,
useLoginWithOAuth,
useLoginWithVeChain,
} from '@vechain/vechain-kit';
const { loginWithPasskey } = useLoginWithPasskey();
const { initOAuth } = useLoginWithOAuth();
const { login: loginWithVeChain } = useLoginWithVeChain();
// OAuth providers: 'google' | 'twitter' | 'apple' | 'discord' | 'github' | 'linkedin'Blockchain Hooks
import { useCurrentBlock, useTxReceipt, useEvents } from '@vechain/vechain-kit';
const { data: block } = useCurrentBlock(); // Auto-refreshes every 10s
const { data: receipt } = useTxReceipt(txId, 5); // Poll for receipt (blockTimeout default: 5)
const { data: events } = useEvents({ // Contract events
abi: contractABI,
address: '0xContract',
eventName: 'Transfer',
filterParams: { from: '0x...' },
});Network & Config Hooks
import { useGetChainId, useGetNodeUrl, useAppConfig } from '@vechain/vechain-kit';
const { data: chainId } = useGetChainId(); // Chain ID from genesis block
const nodeUrl = useGetNodeUrl(); // Current node URL (custom or default)useAppConfig -- Merged Network Config
Returns the full AppConfig for the current network, with any contractAddresses overrides from the provider applied. Prefer this over getConfig() inside React components.
import { useAppConfig } from '@vechain/vechain-kit';
function MyComponent() {
const config = useAppConfig();
// config.b3trContractAddress — uses provider override if set, otherwise network default
// config.vot3ContractAddress
// config.nodeUrl, config.explorerUrl, etc.
}Legal Documents Hook
After configuring legalDocuments on the provider, read agreement status with:
import { useLegalDocuments } from '@vechain/vechain-kit';
const {
documents, // All configured legal documents
agreements, // User's agreement records
documentsNotAgreed, // Documents the user hasn't agreed to yet
hasAgreedToRequiredDocuments, // Boolean — true when all required docs are accepted
} = useLegalDocuments();Oracle, Token, and Domain Hooks
import {
useGetTokenUsdPrice,
useGetCustomTokenInfo,
useGetCustomTokenBalances,
useVechainDomain,
useGetAvatar,
} from '@vechain/vechain-kit';
const { data: vetPrice } = useGetTokenUsdPrice('VET'); // Supported: 'VET', 'VTHO', 'B3TR' (on-chain oracle)
const { data: tokenInfo } = useGetCustomTokenInfo('0xToken');
const { data: balances } = useGetCustomTokenBalances(address, ['0xToken1', '0xToken2']);
const { data: domain } = useVechainDomain('0xAddress'); // address -> domain
const { data: resolved } = useVechainDomain('name.vet'); // domain -> address
const { data: avatar } = useGetAvatar('name.vet');VET Domain Hooks (full list)
Resolution:
useVechainDomain(addressOrDomain)— returns{ address?, domain?, isValidAddressOrDomain }useIsDomainProtected(domain)— returnsboolean(whether the domain is protected from claiming)useGetDomainsOfAddress(address, parentDomain?)— returns{ domains: Array<{ name }> }
Records:
useGetTextRecords(domain)— returns all text records for a domainuseGetAvatar(domain)— returns the avatar image URL directly (converts URI to URL), ornulluseGetAvatarOfAddress(address)— resolves the primary domain, then returns its avatar URL; falls back to a Picasso image if no domain or avatar is setuseGetResolverAddress(domain)— returns the resolver contract address
Mutations:
useUpdateTextRecord({ resolverAddress, onSuccess?, onError?, signerAccountAddress? })— returns{ sendTransaction, isTransactionPending, error }useClaimVeWorldSubdomain({ subdomain, domain, onSuccess?, onError?, alreadyOwned? })— returns{ sendTransaction, isTransactionPending, error }(specific toveworld.vetsubdomains)
VET Domain Text Records
.vet domains support ENS-compatible text records — key-value pairs stored on the resolver (ENSIP-5/18). Common records: display (preferred capitalisation), avatar, description, header (banner image, 1:3 ratio), email, url, location, phone, keywords. Apps can also store custom records with a prefix (e.g. com.discord, org.reddit). Records are read from the name's resolver; write availability depends on the resolver implementation.
NFT and IPFS Hooks
import { useNFTImage, useNFTMetadataUri, useIpfsImage } from '@vechain/vechain-kit';
// Full flow: address → tokenId → metadata → image (all resolved automatically)
const { imageData, imageMetadata, tokenID, isLoading } = useNFTImage({
address: walletAddress,
contractAddress: nftContractAddress,
});
// Just the metadata URI for a known token ID
const { data: metadataUri } = useNFTMetadataUri({ tokenId, contractAddress });
// Resolve any IPFS URI to a gateway URL
const { data: imageUrl } = useIpfsImage(ipfsUri);Sign Messages
import { useSignMessage, useSignTypedData } from '@vechain/vechain-kit';
// Sign a plain message
const { signMessage, isSigningPending, signature } = useSignMessage();
const sig = await signMessage('Hello VeChain');
// Sign EIP-712 typed data
const {
signTypedData,
isSigningPending: isTypedPending,
signature: typedSig,
} = useSignTypedData();
const result = await signTypedData({
domain: { name: 'MyApp', version: '1', chainId: 100009 },
types: { Message: [{ name: 'content', type: 'string' }] },
message: { content: 'Verify wallet ownership' },
primaryType: 'Message',
}, { signer: account?.address }); // signer option required for proper routingCertificate Signing (Wallet Authentication)
To verify wallet ownership for backend JWT flows, use signTypedData with EIP-712. Do not use `useConnex` / `connex.vendor.sign('cert', ...)` — that is deprecated.
Smart account warning: Social login users own a smart account (contract). They sign with their Privy embedded wallet, not the smart account directly. Your backend must verify that the signer address is the owner of the smart account, not just compare it to the connected address.
Frontend hook pattern:
const { signTypedData } = useSignTypedData();
const { account } = useWallet();
const domain = { name: 'MyApp', version: '1' };
const types = {
Authentication: [
{ name: 'user', type: 'address' },
{ name: 'timestamp', type: 'string' },
],
};
const message = { user: account?.address, timestamp: new Date().toISOString() };
const signature = await signTypedData(
{ domain, types, message, primaryType: 'Authentication' },
{ signer: account?.address },
);
// Send { signature, message } to your backendBackend verification:
import { ethers } from 'ethers';
const signerAddress = ethers.verifyTypedData(domain, types, message, signature);
// For wallet users: signerAddress === account address
// For social login users: signerAddress is the embedded wallet —
// verify it is the owner of the smart account on-chainLanguage and Currency Hooks
Bidirectional sync between VeChain Kit settings and your app. Changes in either direction are reflected in both places. Values persist in localStorage (i18nextLng for language, vechain_kit_currency for currency).
Provider props:
<VeChainKitProvider
language="en" // Initial language code
defaultCurrency="usd" // 'usd' | 'eur' | 'gbp'
onLanguageChange={(lang) => {}} // Fired when user changes language in Kit settings
onCurrencyChange={(currency) => {}} // Fired when user changes currency in Kit settings
>Hooks:
import {
useCurrentLanguage,
useCurrentCurrency,
useVeChainKitConfig,
} from '@vechain/vechain-kit';
// Language
const { currentLanguage, setLanguage } = useCurrentLanguage();
setLanguage('fr');
// Currency
const { currentCurrency, setCurrency } = useCurrentCurrency();
setCurrency('eur'); // 'usd' | 'eur' | 'gbp'
// Full config (includes both + other config properties)
const config = useVeChainKitConfig();
config.currentLanguage; // current runtime value
config.currentCurrency; // current runtime value
config.setLanguage('de');
config.setCurrency('gbp');@vechain/contract-getters (Framework-Agnostic Reads)
For read-only blockchain queries outside of React components, use @vechain/contract-getters. It provides typed getters for VeBetterDAO data (B3TR, VOT3 balances, allocation voting, VeBetter Passport), VET domains, ERC-20 tokens, and more. Works in both Node.js and browser environments.
npm install @vechain/contract-getters
# Peer dependencies
npm install @vechain/vechain-contract-types @vechain/sdk-network ethersSimplest usage (no client setup needed — defaults to mainnet):
import { getVot3Balance, getB3trBalance } from '@vechain/contract-getters';
const vot3Balance = await getVot3Balance('0xUserAddress');
const b3trBalance = await getB3trBalance('0xUserAddress');With custom network:
import { getVot3Balance } from '@vechain/contract-getters';
const balance = await getVot3Balance('0xUserAddress', {
networkUrl: 'https://testnet.vechain.org',
});With existing ThorClient (for projects already using VeChain SDK):
import { ThorClient } from '@vechain/sdk-network';
import { VeChainClient, getVot3Balance } from '@vechain/contract-getters';
const thorClient = ThorClient.at('https://testnet.vechain.org');
const vechainClient = VeChainClient.from(thorClient);
const balance = await getVot3Balance('0xUserAddress', { client: vechainClient });Available modules: b3tr, vot3, erc20, vetDomain, allocationVoting, allocationPool, veBetterPassport, relayerRewardsPool.
Use this package when you need blockchain reads in:
- Backend scripts or API routes
- Non-React frontend frameworks
- Utility functions outside of component lifecycle
For React components, prefer the VeChain Kit hooks (useCallClause, useVechainDomain, etc.) instead, as they integrate with React Query for caching and reactivity.
VeChain Kit — Setup & Configuration
When to use
Use when the user asks about: installing VeChain Kit, provider setup, CSS framework choice, Tailwind compatibility, environment variables, login methods, legal documents, ecosystem apps, or common setup issues.
---
Installation
Important: VeChain Kit requires --legacy-peer-deps due to peer dependency conflicts.
Before installing, check the existing project:
- React Query (`@tanstack/react-query`): VeChain Kit hooks depend on it. If the project doesn't have it yet, ask the developer if they want to add it (they almost certainly do — it's required for
useCallClauseand all data-fetching hooks). If the project uses a different data-fetching library (SWR, etc.), flag the potential conflict. - CSS framework: See CSS Framework Choice below — ask whether to keep Tailwind or switch to Chakra UI.
yarn add --legacy-peer-deps @vechain/vechain-kit
# Required peer dependencies
yarn add --legacy-peer-deps @chakra-ui/react@^2.8.2 \
@emotion/react@^11.14.0 \
@emotion/styled@^11.14.0 \
@tanstack/react-query@^5.64.2 \
@vechain/dapp-kit-react@2.1.0-rc.1 \
framer-motion@^11.15.0
# Recommended: pre-built ABIs for VeChain ecosystem contracts
yarn add @vechain/vechain-contract-typesFor npm, use npm install --legacy-peer-deps instead.
Why `@vechain/vechain-contract-types`? It provides TypeChain-generated ABIs and factories for all major VeChain ecosystem contracts (B3TR, VOT3, StarGate, VET domains, smart accounts, etc.). Use these with useCallClause instead of hand-writing ABIs. See the smart-contract-development skill (references/abi-codegen.md) for the full list.
If the project doesn't have React Query yet, also set up the QueryClientProvider:
// app/providers.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{/* VeChainKitProvider goes here */}
{children}
</QueryClientProvider>
);
}CSS Framework Choice
VeChain Kit uses Chakra UI v2 internally for all its modal and UI components. When setting up a new project, ask the developer which approach they prefer:
| Option | Pros | Cons |
|---|---|---|
| Use Chakra UI for the whole app (recommended) | Full visual consistency with VeChain Kit modals, no CSS conflicts, access to Chakra's component library | Must learn Chakra if unfamiliar |
| Keep Tailwind CSS | Developer stays in familiar framework | Requires preflight fix (see below), possible style inconsistencies between app UI and VeChain Kit modals |
If the developer chooses Chakra UI: no extra CSS configuration needed — Chakra's ChakraProvider and VeChain Kit share the same styling engine. Use Chakra components (Box, Button, Text, Flex, etc.) throughout the app.
If the developer keeps Tailwind CSS (especially v4): apply the preflight fix below.
Tailwind CSS v4 Compatibility
Tailwind CSS v4's preflight (CSS reset) conflicts with Chakra UI's styles inside VeChain Kit modals — buttons collapse, inputs lose height, spacing breaks.
Fix: disable Tailwind's preflight. Replace the default Tailwind import with individual imports that skip preflight.css:
/* app/globals.css — BEFORE (broken with VeChain Kit) */
@import "tailwindcss";
/* app/globals.css — AFTER (compatible with VeChain Kit) */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
/* Omit: @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);This removes Tailwind's CSS reset while keeping all utilities and theme variables. Chakra UI applies its own reset inside VeChain Kit components, so they render correctly.
Provider Setup (Next.js App Router)
VeChain Kit must be dynamically imported to prevent SSR issues.
Without own Privy credentials (free shared Privy):
Use vechain for social login — it bundles all social methods (email, Google, passkey, etc.) through VeChain's shared Privy. You cannot use email, google, passkey, or more individually without your own Privy credentials — doing so will throw a configuration error.
// app/providers.tsx
'use client';
import dynamic from 'next/dynamic';
const VeChainKitProvider = dynamic(
() => import('@vechain/vechain-kit').then(mod => mod.VeChainKitProvider),
{ ssr: false }
);
export function Providers({ children }: { children: React.ReactNode }) {
return (
<VeChainKitProvider
network={{ type: 'test' }} // 'main' | 'test' | 'solo'
darkMode={true}
language="en"
loginModalUI={{
logo: '/logo.png',
description: 'My VeChain dApp',
}}
loginMethods={[
{ method: 'veworld', gridColumn: 4, isPrimary: true }, // recommended CTA — filled, dot
{ method: 'vechain', gridColumn: 4 }, // all social login via VeChain's Privy
{ method: 'wallet-connect', gridColumn: 4 }, // WC QR modal triggered programmatically
]}
feeDelegation={{
delegatorUrl: process.env.NEXT_PUBLIC_DELEGATOR_URL,
}}
dappKit={{
allowedWallets: ['veworld', 'wallet-connect'],
walletConnectOptions: {
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID ?? '',
metadata: {
name: 'My dApp',
description: 'A VeChain dApp',
url: typeof window !== 'undefined' ? window.location.origin : '',
icons: [],
},
},
}}
// No privy prop needed — uses VeChain's shared credentials
// Contract address overrides (optional) — for custom deployments on solo/testnet
// contractAddresses={{
// b3trContractAddress: '0x...',
// vot3ContractAddress: '0x...',
// }}
>
{children}
</VeChainKitProvider>
);
}With own Privy credentials (better UX, pick individual methods):
<VeChainKitProvider
// ...same config as above, but with individual login methods and privy prop:
loginMethods={[
{ method: 'veworld', gridColumn: 4, isPrimary: true }, // recommended CTA — filled, dot
{ method: 'google', gridColumn: 4 }, // outline secondary
{ method: 'apple', gridColumn: 4 }, // outline secondary
{ method: 'more', gridColumn: 4 }, // sub-view with overflow wallets / socials / ecosystem
]}
privy={{
appId: process.env.NEXT_PUBLIC_PRIVY_APP_ID ?? '',
clientId: process.env.NEXT_PUBLIC_PRIVY_CLIENT_ID ?? '',
}}
>Then wrap app/layout.tsx with <Providers>.
Environment Variables
Create .env.local with the required variables:
# Required for WalletConnect (get from https://cloud.walletconnect.com)
NEXT_PUBLIC_WC_PROJECT_ID=your_walletconnect_project_id
# Optional: fee delegation (omit to use Generic Delegator — users pay own gas)
NEXT_PUBLIC_DELEGATOR_URL=https://your-delegator.com/delegate
# Optional: own Privy credentials (only if using individual social methods)
NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id
NEXT_PUBLIC_PRIVY_CLIENT_ID=your_privy_client_idCommon Setup Pitfalls
1. SSR errors: VeChain Kit must be dynamically imported with { ssr: false } (shown above). Without this, Next.js will crash during server rendering. 2. Missing `--legacy-peer-deps`: Installation fails without this flag due to Chakra UI v2 peer dependency conflicts. Required with React 19 / Next.js 15+. 3. Tailwind v4 breaks modal: See Tailwind CSS v4 Compatibility above. 4. Using `email`/`google`/`passkey` without Privy credentials: Throws _"Login methods require Privy configuration"_. Use { method: 'vechain' } instead for free social login. 5. Missing WalletConnect project ID: Wallet connection will fail silently. Always provide NEXT_PUBLIC_WC_PROJECT_ID. 6. tsconfig target too low: VeChain SDK uses BigInt literals (0n). Set "target": "ES2020" or higher in tsconfig.json. 7. BigInt serialization error ("Do not know how to serialize a BigInt"): Set wagmi's hashFn as default queryKeyHashFn:
import { hashFn } from 'wagmi/query';
const queryClient = new QueryClient({
defaultOptions: { queries: { queryKeyHashFn: hashFn } },
});8. Restricting wallets: Use dappKit: { allowedWallets: ['veworld'] } to show only VeWorld (omit 'wallet-connect' if you don't need WalletConnect and don't have a project ID). 9. Privy popup blocking: Browsers block popups that open after an async call. Pre-fetch all data before triggering sendTransaction so the Privy signing popup opens synchronously. 10. Missing `ColorModeScript`: If VeChain Kit modals render with wrong colors, add <ColorModeScript initialColorMode="dark" /> inside your ChakraProvider. 11. CSS conflicts with Bootstrap or custom CSS: Use CSS layers — @layer vechain-kit, host-app; — and wrap your framework styles in @layer host-app { ... }.
Testing (mocking VeChain Kit hooks)
jest.mock('@vechain/vechain-kit', () => ({
useWallet: () => ({ account: { address: '0x123...' }, isConnected: true }),
useCallClause: () => ({ data: [BigInt('1000000000000000000')], isLoading: false, error: null }),
}));Sub-path Exports
VeChain Kit exposes additional exports via sub-paths:
// Contract factories (re-exports from @vechain/vechain-contract-types)
import { IB3TR__factory } from '@vechain/vechain-kit/contracts';
// Utility functions
import { humanAddress } from '@vechain/vechain-kit/utils';
// Network config (contract addresses, chain IDs)
import { getConfig, useAppConfig } from '@vechain/vechain-kit';
const b3trAddress = getConfig('main').b3trContractAddress;
// In React components, prefer useAppConfig() — it respects contractAddresses overrides
const config = useAppConfig();
const b3tr = config.b3trContractAddress;Login Methods
From v2.7 the kit owns the entire VeWorld and Sync2 connection flow end-to-end — no hand-off to dapp-kit's native picker. WalletConnect still uses WalletConnect's own QR modal (triggered programmatically). The legacy dappkit entry is preserved for backwards compatibility.
| Method | Description | Requires Privy | Gated by dappKit.allowedWallets |
|---|---|---|---|
veworld | Custom VeWorld flow + the kit's "Waiting for signature…" view. Primary CTA (filled, recommended dot) | No | Yes — needs 'veworld' |
sync2 | Custom Sync2 flow + same waiting view | No | Yes — needs 'sync2' |
wallet-connect | Triggers WalletConnect's QR modal programmatically (kit's loading view sits behind) | No | Yes — needs 'wallet-connect' |
vechain | All social login via VeChain's shared Privy (free; slightly worse UX — VeChain branding, extra redirect) | No | — |
ecosystem | Footer button → sub-view of x2earn ecosystem apps | No | — |
email | Inline email pill + 6-digit code modal | Yes | — |
passkey | Privy WebAuthn | Yes | — |
google | Google OAuth (full-color "G") | Yes | — |
apple | Apple OAuth | Yes | — |
github | GitHub OAuth | Yes | — |
more | "More options ⌄" link footer → sub-view with overflow wallets / socials (incl. Privy fallback for Twitter/Discord/etc.) / ecosystem apps | Yes (for socials) | — |
dappkit _(legacy)_ | Opens dapp-kit's native picker modal. Preserved for backwards compatibility — prefer the granular methods above | No | — |
Defaults (when loginMethods is omitted):
- With
privy:[veworld, google, apple, more] - Without
privy:[veworld, sync2, wallet-connect]
Important: Without the privy prop, email, passkey, and sms throw a configuration error (no whitelabel equivalent for those — they need to run inline at the dApp's origin). Everything else (vechain, google, apple, twitter, discord, github, tiktok, line, more, wallet methods) works without your own Privy account.
Grid layout: gridColumn controls the width of each login button in a 4-column grid. Use 4 for full width, 2 for half width.
Recommended CTA: mark one entry with isPrimary: true to render it as the recommended CTA — filled inverted surface + green "recommended" dot. If no entry sets isPrimary, the kit auto-highlights the first visible method. isPrimary on more is ignored (it's a footer link). The filled treatment currently supports veworld, google, apple, and github; other methods stay outline even if marked primary.
Driving a single wallet from custom UI:
import { useConnectWithDappKitSource, useModal } from '@vechain/vechain-kit';
const { setConnectModalContent, openConnectModal } = useModal();
const { connect } = useConnectWithDappKitSource('veworld', setConnectModalContent);
// ^^^^^^^^ 'veworld' | 'sync2' | 'wallet-connect'
<button onClick={async () => { openConnectModal(); await connect(); }}>Connect VeWorld</button>Ecosystem Apps
Filter which ecosystem apps appear when using { method: 'ecosystem' }:
<VeChainKitProvider
loginMethods={[
{ method: 'ecosystem', gridColumn: 4 },
]}
ecosystemApps={{
allowedApps: ['app-id-1', 'app-id-2'], // App IDs from the Privy dashboard
}}
>Contract Address Overrides
Override default contract addresses for custom deployments (e.g., solo or testnet with your own B3TR/VOT3 instances). Accepts Partial<AppConfig> — only provided fields are overridden:
<VeChainKitProvider
network={{ type: 'solo' }}
contractAddresses={{
b3trContractAddress: '0x026771d1be764467f8bdb78bb230df10c924b00d',
vot3ContractAddress: '0xf7a08af15cb3501feee53ebe11f4428a966fa459',
// Any AppConfig field can be overridden
}}
>Access the merged config (defaults + overrides) in components with useAppConfig:
import { useAppConfig } from '@vechain/vechain-kit';
const config = useAppConfig();
const b3trAddress = config.b3trContractAddress; // overridden value if provideduseAppConfig is preferred over getConfig() inside React components, as it respects provider overrides. getConfig() only returns built-in network defaults.
Legal Documents (Optional)
Prompt users to accept Terms & Conditions, Privacy Policy, or Cookie Policy on wallet connect. Agreements are stored in local storage per wallet address + document type + version + URL. Incrementing version re-prompts users.
<VeChainKitProvider
legalDocuments={{
allowAnalytics: true, // Optional: prompt for VeChainKit tracking consent
termsAndConditions: [
{
displayName: 'MyApp T&C',
url: 'https://myapp.com/terms',
version: 1,
required: true, // Must accept to proceed
},
],
privacyPolicy: [
{
url: 'https://myapp.com/privacy',
version: 1,
required: false, // Optional: user can skip
},
],
cookiePolicy: [
{
url: 'https://myapp.com/cookies',
version: 1,
required: false,
},
],
}}
>Each document entry supports: displayName (optional label), url, version, required (boolean).
VeChain Kit — Social Login & Smart Accounts
When to use
Use when the user asks about: social login, smart accounts, account abstraction, Privy setup, fee delegation for social login, or DIY social login with dapp-kit.
---
Smart Accounts
- Social login users get a Smart Account (account abstraction) via CREATE2
- Deterministic address (can receive tokens before deployment)
- V3 required for multi-clause and replay protection
- Check:
useUpgradeRequiredForAccount - Factory addresses (must use the official factory for ecosystem compatibility):
- Mainnet:
0xC06Ad8573022e2BE416CA89DA47E8c592971679A - Testnet:
0x713b908Bcf77f3E00EFEf328E50b657a1A23AeaF
Privy Setup (Optional for Social Login)
VeChain Kit ships with social login out of the box — no Privy account is required. There are two paths; pick the one that fits your needs.
Option A: Use VeChain's whitelabel cross-app host (free, no setup, recommended for most apps)
Omit the privy prop entirely. The kit routes social logins through VeChain's whitelabel popup (cross-app-connect), which runs on VeChain branding and gives the user one identity across every kit-integrated dApp.
What works without your own Privy:
{ method: 'vechain' }— a single "Continue with VeChain" button that opens the popup picker.{ method: 'google' },{ method: 'apple' },{ method: 'twitter' },{ method: 'discord' },{ method: 'github' },{ method: 'tiktok' },{ method: 'line' }— direct buttons that open the popup pre-selected on that provider (one-tap login).useLoginWithOAuth().initOAuth({ provider })for the same provider set, driven from your own UI.
What still requires your own Privy (Option B):
{ method: 'email' },{ method: 'passkey' },{ method: 'sms' }— these have to run inside your dApp's origin and need its own Privy credentials.- Custom OAuth providers not in the kit's whitelabel set (LinkedIn, Spotify, Instagram, etc.).
Calling an unsupported method without privy throws a configuration error pointing to the supported set.
Option B: Use your own Privy account (full control)
Create an app at privy.io, retrieve your App ID and Client ID from the App Settings tab, and pass them to VeChainKitProvider (see setup guide):
<VeChainKitProvider
privy={{
appId: process.env.NEXT_PUBLIC_PRIVY_APP_ID!,
clientId: process.env.NEXT_PUBLIC_PRIVY_CLIENT_ID!,
}}
>The privy prop also accepts appearance, embeddedWallets, and other Privy SDK options as pass-through configuration.
Option A vs Option B trade-offs
| Whitelabel cross-app (A) | Self-hosted Privy (B) | |
|---|---|---|
| Cost | Free | Privy pricing |
| Setup | Zero | Privy dashboard config + env vars |
| Branding | VeChain-branded popup window | Your branding inside your dApp |
| Login surface | Brief popup window (handles OAuth + posts result back) | Inline modal — no popup |
| User wallet | Shared across all kit-integrated dApps (one VeChain identity) | Scoped to your dApp |
| Methods available | Google, Apple, X, Discord, GitHub, TikTok, LINE, plus the picker for everything else | Everything Privy supports (email, passkey, SMS, additional OAuth providers, …) |
| Transaction prompts | Popup confirmation per signature | No UI confirmations |
| Cross-app identity | Built-in | User has to ecosystem-link |
| Security ownership | VeChain owns the Privy account | You secure your Privy account |
Security: If self-hosting Privy, review the implementation checklist and CSP guide.
Accessing Privy directly: VeChain Kit re-exports Privy hooks — import from the kit, not from @privy-io/react-auth:
import { usePrivy } from '@vechain/vechain-kit';
const { user } = usePrivy();Fee Delegation for Social Login
VeChain Kit v2 auto-enables the Generic Delegator by default -- users pay their own gas in VET, VTHO, or B3TR. No feeDelegation config is required.
To improve UX, you can optionally sponsor transactions so users pay nothing:
<VeChainKitProvider feeDelegation={{ delegatorUrl: 'https://your-delegator.com/delegate' }}>See the vechain-core skill (references/fee-delegation.md) for Generic Delegator gas estimation, per-transaction sponsorship, and vechain.energy setup.
Pre-fetch Data Before Transactions
Fetching during sendTransaction blocks popups for social login:
// GOOD: data ready before transaction
const { data: balance } = useCallClause({ ... });
const handleSend = () => sendTransaction(clauses);
// BAD: fetching inside handler
const handleSend = async () => {
const balance = await fetchBalance(); // May block popup
sendTransaction(clauses);
};---
DIY Social Login with dapp-kit + Privy (Not Recommended)
An alternative to VeChain Kit's built-in social login is using dapp-kit while handling Privy integration, smart account management, and EIP-712 signing yourself. This adds significant complexity and is not recommended unless you have a specific reason VeChain Kit cannot work for your use case.
VeChain Kit vs DIY Comparison
| Concern | VeChain Kit (recommended) | DIY with dapp-kit |
|---|---|---|
| Smart account contracts | Uses official pre-deployed factory | Must deploy your own OR integrate official factory |
| EIP-712 signing | Automated in useSendTransaction | Manual typed data construction |
| Account deployment detection | Built-in (lazy deploy on first tx) | Custom logic required |
| Replay protection | Built-in nonce handling (V3) | Manual nonce management |
| Version upgrades (V1→V3) | useUpgradeRequiredForAccount + modal | Must track yourself |
| Batch/multi-clause | Automated via executeBatchWithAuthorization | Must build manually |
| iOS/Android signing | Handled (custom domain separator) | Not addressed in tutorial |
| Cross-app compatibility | Supported via @privy-io/cross-app-connect | Not supported |
| Provider setup | Single <VeChainKitProvider> | Nested <PrivyProvider> + custom <VeChainAccountProvider> |
Critical: Use the Official Smart Accounts Factory
If you take the DIY path, you must use the official vechain/smart-accounts factory (0xC06Ad... mainnet / 0x713b9... testnet). Deploying your own factory (as the tutorial does) creates smart accounts that are not compatible with VeChain Kit, VeWorld, or other VeChain ecosystem apps. Users would have different addresses across apps.
See Smart Accounts documentation for factory details.
What You Must Implement Yourself
1. EIP-712 typed data construction -- build and sign authorization payloads for executeWithAuthorization 2. Lazy account deployment -- detect undeployed accounts and inject factory creation clauses on first transaction 3. Fee delegation integration -- separate sponsor signature flow 4. Nonce management -- for executeBatchWithAuthorization replay protection 5. Version migration -- the factory has evolved V1→V3 (V2 was skipped); handle upgrades 6. HTTPS requirement -- Privy uses crypto.subtle, requiring HTTPS even in development (e.g., ngrok) 7. Ephemeral wallet for submission -- generate a random wallet as the transaction entry point; actual auth comes from the Privy-signed EIP-712 message
When DIY Might Be Justified
- You need custom smart account logic beyond what SimpleAccount V3 provides
- You need full control over the signing/submission pipeline
- You are building for a non-React framework where VeChain Kit cannot run
VeChain Kit — Theming & Compatibility
When to use
Use when the user asks about: theming VeChain Kit, customizing colors/fonts/buttons, Chakra UI compatibility, bottom sheet on mobile, glass effects, or webpack fallbacks.
---
Theming
Minimal config: set modal.backgroundColor and textColor — all other colors auto-derive. Import VechainKitThemeConfig for type safety.
import type { VechainKitThemeConfig } from '@vechain/vechain-kit';
const theme: VechainKitThemeConfig = {
modal: {
backgroundColor: isDarkMode ? '#1f1f1e' : '#ffffff',
useBottomSheetOnMobile: true, // Slide-up bottom sheet on mobile instead of centered modal
// border, backdropFilter, rounded are optional
},
textColor: isDarkMode ? 'rgb(223, 223, 221)' : '#2e2e2e',
// Brand accent — spinner top arc, focus rings, "Waiting for signature…"
// headline in the connect modal, and the email-submit link when valid.
// Defaults: '#3b82f6' (light) / '#60a5fa' (dark).
accent: '#ff6600',
overlay: {
backgroundColor: 'rgba(0, 0, 0, 0.6)',
blur: 'blur(3px)',
},
buttons: {
primaryButton: { bg: '#3182CE', color: 'white', border: 'none' },
secondaryButton: { bg: 'rgba(255,255,255,0.05)', color: '#fff', border: 'none' },
tertiaryButton: { bg: 'transparent', color: '#fff', border: 'none' },
loginButton: { bg: 'transparent', color: '#fff', border: '1px solid rgba(255,255,255,0.1)' },
},
fonts: {
family: 'Inter, sans-serif',
sizes: { small: '12px', medium: '14px', large: '16px' },
weights: { normal: 400, medium: 500, bold: 700 },
},
effects: {
glass: { enabled: true, intensity: 'low' }, // 'low' | 'medium' | 'high'
},
};
<VeChainKitProvider theme={theme} {...otherProps}>Theme API reference
| Prop | Shape | Notes |
|---|---|---|
modal | { backgroundColor, border, backdropFilter, rounded, useBottomSheetOnMobile } | Modal container. backgroundColor auto-derives card (80%), header (90%), secondary/tertiary, and border colors. useBottomSheetOnMobile: slide-up bottom sheet on mobile |
textColor | string | Auto-derives primary (100%), secondary (70%), tertiary (50%) text |
accent | string | Brand accent. Drives the connect modal's spinner top arc, focus rings, "Waiting for signature…" headline, and the email-submit link when the address is valid. Default #3b82f6 (light) / #60a5fa (dark) |
overlay | { backgroundColor, blur } | Modal overlay backdrop |
buttons | { primaryButton, secondaryButton, tertiaryButton, loginButton } | Each: { bg, color, border, backdropFilter?, rounded? } |
fonts | { family, sizes?, weights? } | sizes: { small, medium, large }. weights: { normal, medium, bold }. Scoped to Kit components only — does not affect host app |
effects | { glass: { enabled, intensity } } | Glass morphism; intensity: 'low' / 'medium' / 'high' |
Common mistakes:
buttons.primary.backgrounddoes not exist — usebuttons.primaryButton.bgfont.familydoes not exist — usefonts.familyhoverBgdoes not exist in the types
Chakra UI v3 compatibility
VeChain Kit uses Chakra UI v2 internally. When the host app uses Chakra v3, pin `@chakra-ui/react` to an exact working version (currently 3.30.0). Newer v3 releases can change CSS variable generation and break VeChain Kit's button/modal styling (wrong colors, missing backgrounds). Do NOT use ^ ranges like ^3.26.0.
useToken returns a snapshot, not a CSS variable
Chakra v3's useToken('colors', 'bg.primary') returns the resolved literal color at render time (e.g. #1B1D1F), NOT a CSS variable reference. If you pipe that snapshot into the Kit's theme prop, the Kit's modal/card/sticky-header colors freeze in whichever mode Chakra evaluated first and stop tracking host theme toggles (next-themes, html.dark, etc.). Only Kit components reading useVeChainKitConfig().darkMode directly (e.g. the VeWorld button) will react.
Wrong:
// ❌ freezes to whatever mode Chakra evaluated first
const [bgPrimary, primaryDefault] = useToken('colors', [
'bg.primary',
'actions.primary.default',
])
<VeChainKitProvider theme={{ modal: { backgroundColor: bgPrimary }, ... }} />Right — use Chakra v3's `sys.token.var(...)` resolver so the Kit gets a `var(...)` reference that flips at paint time:
import { useChakraContext } from '@chakra-ui/react'
const sys = useChakraContext()
const tokVar = (p: string) => sys.token.var(`colors.${p}`) as string
const bgPrimary = tokVar('bg.primary') // 'var(--vbd-colors-bg-primary)'
const primaryDefault = tokVar('actions.primary.default')
// …etc
<VeChainKitProvider theme={{ modal: { backgroundColor: bgPrimary }, ... }} />Hardcoding 'var(--vbd-colors-bg-primary)' strings works too if the cssVarsPrefix is fixed.
To verify after wiring: in DevTools, --chakra-colors-vechain-kit-modal should resolve to var(--your-prefix-...) (and switch on theme toggle), not to a hex literal.
A full repro lives at examples/next-chakra-v3/ in vechain/vechain-kit.
Webpack fallbacks for Next.js
Some VeChain packages (e.g. @vechain/vebetterdao-relayer-node) import Node.js modules (fs, net, tls). For Next.js client-side builds, add webpack fallbacks in next.config.js:
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = { ...config.resolve.fallback, fs: false, net: false, tls: false }
}
return config
},Translations and VeChain Kit
When the host app uses react-i18next and VeChain Kit, keep both in sync so that changing language in the app updates Kit UI and changing language in Kit (e.g. wallet modal) updates the app.
Bi-directional language sync
Host app → VeChain Kit
When the user changes language in the host app (e.g. footer selector calling i18n.changeLanguage(...)), notify Kit:
- Inside the
VeChainKitProvidertree, subscribe toi18n.on("languageChanged", ...)and call Kit'ssetLanguage(lng)fromuseCurrentLanguage(). - Do this in a small child component that has access to both
useTranslation()anduseCurrentLanguage().
VeChain Kit → host app
When the user changes language inside Kit (e.g. wallet modal), update the host app:
- Pass into
VeChainKitProvider:language={i18n.language}andonLanguageChange={(language) => { if (i18n.language !== language) i18n.changeLanguage(language) }}.
Implementation pattern
// 1) Child: sync app i18n → Kit
function LanguageSync({ children }: { children: React.ReactNode }) {
const { i18n } = useTranslation()
const { setLanguage: setKitLanguage } = useCurrentLanguage()
useEffect(() => {
const handle = (lng: string) => setKitLanguage(lng)
i18n.on("languageChanged", handle)
return () => i18n.off("languageChanged", handle)
}, [i18n, setKitLanguage])
return <>{children}</>
}
// 2) Provider: pass current language and Kit → app handler
export function VechainKitProviderWrapper({ children }) {
const { i18n } = useTranslation()
const handleLanguageChange = (language: string) => {
if (i18n.language !== language) i18n.changeLanguage(language)
}
return (
<VeChainKitProvider
language={i18n.language}
onLanguageChange={handleLanguageChange}
{/* ...other props */}
>
<LanguageSync>{children}</LanguageSync>
</VeChainKitProvider>
)
}Host app language selector: call i18n.changeLanguage(value); sync to Kit happens via languageChanged.
Persist language across refreshes
In your i18n.ts, check localStorage first to avoid losing the selected language on page reload:
const customLanguageDetector = {
name: 'customDetector',
lookup: () => {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('i18nextLng');
if (stored && supportedLanguages.includes(stored)) return stored;
}
const browserLang = navigator.language.split('-')[0];
if (supportedLanguages.includes(browserLang)) return browserLang;
return 'en';
},
cacheUserLanguage: (lng: string) => {
localStorage.setItem('i18nextLng', lng);
},
};Optional: dayjs locale
If you use dayjs: i18n.on("languageChanged", (lng) => { dayjs.locale(lng === "tw" ? "zh-tw" : lng) }).
Pre-commit and ESLint (missing / unused translations)
Pre-commit
- lint-staged: Often runs ESLint + Prettier on staged
.ts/.tsxand Prettier on.json. No i18n-specific step by default. - Unused keys in en.json: A script can find keys in
en.jsonthat are never used in code (t("..."),i18nKey="..."). Run it in pre-commit when translation files or code change (e.g. whenen.jsonor anysrc/i18n/languages/*.jsonis staged). Exit non-zero if unused keys exist so the commit fails. - Missing keys in other locales: Add a script that compares each locale's keys to
en.jsonand exits with an error if any key is missing or extra. Run from pre-commit (when i18n files staged) or CI.
ESLint
- Many projects do not use
eslint-plugin-i18next(or similar). To highlight missing translations: (1) enable an unused-keys script in pre-commit and a "missing keys per locale" script, or (2) add an i18n ESLint plugin and point it at the translation files.
Summary
| Check | How to enable |
|---|---|
| Unused keys in en.json | Script that scans code for t("key") / i18nKey and compares to en.json; run in pre-commit or CI |
| Missing/extra keys in other locales | Script that compares each locale JSON to en.json; run in pre-commit or CI |
| ESLint missing keys | Optional: add eslint-plugin-i18next (or similar) and configure |