Wallet Deployment
Deploy the corporation wallet, install the corporate policy and executor, and create the corporation on-chain.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Integration overview |
| Credentials | partner_id and the ContractRegistry address |
Overview
A corporation is an Account Abstraction wallet. There is no separate corporate entity on-chain — the
wallet address is the corporation address, and it is the value every corporation-scoped API call is
evaluated against.
Before the corporation can be registered through the API, three things must be true on-chain:
- The corporation wallet is deployed.
CorporateValidatoris its root validator's signer module,ExecutionDelayCorporatePolicyis that
validator's single policy, and theFundsManagementexecutor is installed.createCorporationForCorporateWallet(partnerId, wallet)has been called, creating the corporation
record and assigning the creator the on-chainOwnerrole.
A second registration call,
createCorporation, exists and is obsolete. It produces a wallet that
only one key can ever sign for, which permanently excludes employees from operating the corporation
wallet. It is documented at the end of this page for corporations that
were registered with it. Do not use it for a new corporation.
Retail and Corporate Wallets Are Not Interchangeable
| Retail user wallet | Corporation wallet | |
|---|---|---|
| Registration contract | Accounts | CorporateAccounts |
| Registration call | createUserAccountWithWallet(parentEntity) | createCorporationForCorporateWallet(parentEntity, wallet) |
| Root validator signer | ECDSA signer | CorporateValidator |
| Root validator policy | ExecutionDelayPolicy | ExecutionDelayCorporatePolicy |
| Executor module | FundsManagement | FundsManagement |
| Identity in the API | The user's EOA (X-User-Wallet) | The wallet address (corporation_address) |
A wallet configured with the retail
ExecutionDelayPolicyis refused with
Policy is not the execution delay policy. ResolveExecutionDelayCorporatePolicyfrom the
ContractRegistry, notExecutionDelayPolicy.
Components
Kernel v3.1 Smart Account
The corporation wallet itself. A smart contract wallet that:
- Is validated by a signer module rather than a fixed key
- Supports modular plugins for validators, executors and policies
- Enables gas sponsorship via paymasters
- Allows batched transactions
CorporateValidator
The signer module behind the wallet's root validator, and the component that makes a corporation a
multi-person entity rather than a single-key wallet.
On every UserOperation it recovers the signing address and asks CorporateAccounts whether that address
holds a sufficient on-chain role:
| Operation | Permissions accepted |
|---|---|
Account-change operations — installModule, changeRootValidator, installValidation, removeValidations | Su, AccountSu |
| Every other operation | Su, TransactionSu, TransactionCreate |
Recovery is attempted against the raw hash and then the EIP-191 prefixed hash, so both signing
conventions are accepted.
This is the mechanism by which an employee can act for the corporation. A wallet whose root
validator signs with a plain ECDSA key never consults employee roles, so no employee can ever sign for
it.
Install data is 36 bytes: bytes16 parentEntity followed by address creator. The creator recorded
here becomes the corporation's owner and is the address sent as X-User-Wallet at registration.
FundsManagement Executor
Module type 2, installed on the wallet. Grants the Wirex oracle permission to execute the specific
operations the platform performs on the corporation's behalf — card payments, bank transfer settlement,
FX. Without it, registration reverts with Missing executor module, and no funded flow works.
ExecutionDelayCorporatePolicy
Installed as the root validator's policy. Enforces the time delay on sensitive operations and carries
the parentEntity binding that registration writes.
The policy configuration must resolve to exactly one policy entry. A wallet whose permission config
carries a different number of policies is refused with Policy data is not as it should be.
CorporateAccounts Registration
Writes the corporation record keyed by (parentEntity, walletAddress), sets its status to PENDING,
assigns the creator the on-chain Owner role, and binds the parentEntity into the policy.
Registering the Corporation
function createCorporationForCorporateWallet(bytes16 parentEntity, address wallet) external;| Argument | Description |
|---|---|
parentEntity | Your partner_id, as bytes16 |
wallet | Address of the corporation wallet |
Sent from: any address. The call is permissionless, and deliberately so: CorporateValidator
cannot validate a UserOperation until the corporation and its owner exist, so the wallet cannot
register itself. The owner is not taken from msg.sender — it is read from the creator recorded in
CorporateValidator when the module was installed.
Required Wallet State
Configure the wallet before calling. Registration verifies every item below and reverts on the first
one that fails.
| Requirement | Detail |
|---|---|
| Kernel v3.1 account deployed | The account address becomes corporation_address |
FundsManagement installed | Module type 2 |
| Root validator is a permission validator | Its policyData must hold exactly one entry |
That policy is ExecutionDelayCorporatePolicy | Resolved from the ContractRegistry |
That validator's signer module is CorporateValidator | Resolved from the ContractRegistry |
CorporateValidator install data | bytes16 parentEntity ++ address creator, 36 bytes |
The stored parentEntity matches the argument | Otherwise ParentEntity mismatch |
Encoding the signer install data:
import { concatHex, type Hex } from 'viem';
// 36 bytes: bytes16 parentEntity ++ address creator
const corporateValidatorInstallData: Hex = concatHex([
partnerId, // 0x + 32 hex chars — bytes16
ownerEoaAddress, // 0x + 40 hex chars — the address that becomes the corporation owner
]);ownerEoaAddress is the address the contract records as creator, assigns the on-chain Owner role,
and returns as owner_address from GET /api/v1/corporations. It is the value sent as X-User-Wallet
when registering and logging in.
The ZeroDev permission-validator wiring for a custom signer module is not exposed by
@wirexapp/wpay-baas-sdk. Request the corporation wallet setup helper from Wirex rather than
assembling it from the retail SDK — see Using the Wirex SDK.
Registration Call
import { encodeFunctionData } from 'viem';
import { CorporateAccountsAbi } from './abi';
// Permissionless — send from any funded address
const txHash = await walletClient.sendTransaction({
to: contracts.corporateAccounts,
data: encodeFunctionData({
abi: CorporateAccountsAbi,
functionName: 'createCorporationForCorporateWallet',
args: [partnerId, corporationWalletAddress],
}),
});The two addresses this produces map onto the API as follows, and confusing them is the most common
integration failure:
| Address | API usage |
|---|---|
| Corporation wallet address | corporation_address in the request body of register and login |
| Creator (EOA) address | X-User-Wallet header, and owner_address in GET /api/v1/corporations |
Registration Checks
The call verifies, in order:
| Check | Revert message |
|---|---|
parentEntity is non-zero | Invalid parent entity |
wallet is non-zero | Invalid wallet address |
FundsManagement is installed as a type-2 module | Missing executor module |
| The root validator resolves to exactly one policy | Policy data is not as it should be |
That policy is ExecutionDelayCorporatePolicy | Policy is not the execution delay policy |
The permission signer is CorporateValidator | Signer is not CorporateValidator |
| The validator's stored parent entity matches | ParentEntity mismatch |
| The validator holds a creator address | Creator not set in validator |
| No corporation exists for this key | Corporation already exists |
Verification
Read the corporation record back before calling the API:
function getCorporation(bytes16 parentEntity, address corporation)
external view returns (Corporation memory);const corporation = await publicClient.readContract({
address: contracts.corporateAccounts,
abi: CorporateAccountsAbi,
functionName: 'getCorporation',
args: [config.partnerId, corporationAddress],
});
// corporation.status !== 0 means the corporation exists on-chainThe returned struct:
| Field | Description |
|---|---|
creator | The owner address recorded at creation |
status | 0 UNKNOWN, 1 PENDING, 2 ACTIVE, 3 BLOCKED, 4 DELETED |
verificationStatus | 0 UNKNOWN, 1 APPLIED, 2 IN_REVIEW, 3 APPROVED, 4 CANCELLED, 5 REJECTED |
walletStatus | 0 UNKNOWN, 1 CONFIRMED, 2 REJECTED |
status and verificationStatus are advanced by the Wirex KYC oracle as KYB progresses. They are not
partner-writable.
On-Chain Roles
Registration assigns the creator the on-chain Owner role. Six default roles are seeded in the
contract, and they are the same role ids the API returns from GET /api/v1/roles with
"is_global": true.
| Role | Role id | On-chain permissions |
|---|---|---|
| Owner | ed7a5845-8727-4b06-9ba0-7d43ddc9e6aa | Su |
| Admin | 31eba1db-0809-4d44-a479-0b696c4a2603 | CardSu, EmployeeSu, TransactionSu, AccountSu |
| Treasury | ce3de0cc-bd42-4bc8-829c-e395fab506ef | TransactionSu, CardSu, AccountSu |
| HR | a1b2c3d4-e5f6-0718-29a0-b1c2d3e4f506 | EmployeeSu |
| Member | 7386b48f-df9c-49fe-8ccc-38b5b0b55932 | TransactionCreate, TransactionView, CardView, AccountView, EmployeeView |
| Viewer | 240d596c-8973-4203-827f-a2c994bceafa | TransactionView, CardView, AccountView, EmployeeView |
Employee role changes on-chain are made by the Wirex oracle through createOrUpdateEmployee, driven by
the API calls described in Employees. Partners do not call it directly.
These roles are what CorporateValidator reads when it validates a UserOperation. On a wallet
registered through the obsolete createCorporation path they are written and never read — see
Obsolete: createCorporation.
Using the Wirex SDK
@wirexapp/wpay-baas-sdk exposes a corporate registration call and a corporate membership check:
const isRegistered = await sdk.crypto.accountContract.isWalletInCorporateAccounts();
if (!isRegistered) {
await sdk.crypto.accountContract.registerInCorporateAccounts();
}isWalletInCorporateAccounts() reads getCorporation and is safe to use on any corporation wallet.
registerInCorporateAccounts()takes the obsolete path. It encodescreateCorporation(companyId)
and sends it from the smart wallet, producing a single-key corporation — see
Obsolete: createCorporation. It does not call
createCorporationForCorporateWallet.
The SDK's wallet setup helpers install the retail policy.
sdk.crypto.accountAbstraction.signInPolicyAndExecutor()andsignInPolicy()resolve
ExecutionDelayPolicy, notExecutionDelayCorporatePolicy, and neither installs
CorporateValidator. A corporation wallet cannot be assembled from these helpers.
Request the corporation wallet setup and registration helper from Wirex. Do not build corporate
onboarding on the retail SDK path.
Obsolete: createCorporation
Obsolete. Do not use for a new corporation. This path is documented for corporations already
registered with it. A corporation created this way cannot use the platform's role model: employees
can be invited through the API and given on-chain roles, but none of them can ever sign a
UserOperation for the corporation wallet. Only the single key behind the root validator can act,
for the life of the wallet.
function createCorporation(bytes16 parentEntity) external;| Argument | Description |
|---|---|
parentEntity | Your partner_id, as bytes16 |
Sent from: the corporation wallet. msg.sender becomes the corporation address, and the ECDSA
signer behind the root validator becomes the owner.
Why It Is Obsolete
createCorporation requires the root validator's signer to be a plain ECDSA signer — it reads the
signing address directly off that module, and reverts if it cannot. That requirement is what makes the
wallet single-key:
createCorporationForCorporateWallet | createCorporation — obsolete | |
|---|---|---|
| Root validator signer | CorporateValidator | A plain ECDSA signer |
| What validates a UserOperation | The signer's on-chain role in CorporateAccounts | One fixed key |
| Employees can sign for the corporation | Yes, per their role | Never |
| On-chain roles are consulted | Yes | No — the role records exist and are inert |
| Separation of duties | Roles gate transactions and account changes independently | None |
The two paths are mutually exclusive by wallet configuration and each rejects the other's wallet:
createCorporation reverts on a CorporateValidator wallet, and
createCorporationForCorporateWallet reverts with Signer is not CorporateValidator on an ECDSA
wallet. The choice is made when the root validator is installed, not at registration.
There is no API flow that migrates a corporation from one to the other. Contact Wirex if you hold a
corporation registered this way.
The Legacy Flow
Retained for reference. Policy change, executor installation and corporation creation are batched into
one UserOperation.
import { createZeroDevPaymasterClient, KernelV3_1AccountAbi } from '@zerodev/sdk';
import { createKernelAccount, createKernelAccountClient } from '@zerodev/sdk';
import { signerToEcdsaValidator } from '@zerodev/ecdsa-validator';
import { getEntryPoint, KERNEL_V3_1, VALIDATOR_TYPE } from '@zerodev/sdk/constants';
import { toPermissionValidator } from '@zerodev/permissions';
import { toSudoPolicy } from '@zerodev/permissions/policies';
import { toECDSASigner } from '@zerodev/permissions/signers';
import {
createPublicClient, http, encodeFunctionData, concatHex, pad,
zeroAddress, zeroHash, type PrivateKeyAccount,
} from 'viem';
import { base } from 'viem/chains';
import { ContractRegistryAbi, CorporateAccountsAbi } from './abi';
// Resolve the corporate contracts from the registry
async function resolveCorporateContracts(publicClient: any, registryAddress: `0x${string}`) {
const byName = (name: string) =>
publicClient.readContract({
address: registryAddress,
abi: ContractRegistryAbi,
functionName: 'contractByName',
args: [name],
}) as Promise<`0x${string}`>;
return {
corporateAccounts: await byName('CorporateAccounts'),
fundsManagement: await byName('FundsManagement'),
executionDelayCorporatePolicy: await byName('ExecutionDelayCorporatePolicy'),
};
}
async function deployCorporationWallet(
signer: PrivateKeyAccount,
config: {
chainId: number; rpcUrl: string; bundlerRpc: string; paymasterRpc: string;
contractRegistryAddress: `0x${string}`; partnerId: `0x${string}`;
}
) {
const publicClient = createPublicClient({ chain: base, transport: http(config.rpcUrl) });
const contracts = await resolveCorporateContracts(publicClient, config.contractRegistryAddress);
const paymasterClient = createZeroDevPaymasterClient({
chain: base,
transport: http(config.paymasterRpc),
});
const simpleValidator = await signerToEcdsaValidator(publicClient, {
signer,
entryPoint: getEntryPoint('0.7'),
kernelVersion: KERNEL_V3_1,
});
const kernelAccount = await createKernelAccount(publicClient, {
entryPoint: getEntryPoint('0.7'),
kernelVersion: KERNEL_V3_1,
plugins: { sudo: simpleValidator },
});
const kernelClient = createKernelAccountClient({
account: kernelAccount,
chain: base,
bundlerTransport: http(config.bundlerRpc),
paymaster: {
getPaymasterData: (userOperation) =>
paymasterClient.sponsorUserOperation({ userOperation }),
},
});
// 1. Root validator = permission validator with the CORPORATE delay policy
const rootPolicy = toSudoPolicy({
policyAddress: contracts.executionDelayCorporatePolicy,
});
const permissionValidator = await toPermissionValidator(publicClient, {
entryPoint: getEntryPoint('0.7'),
signer: await toECDSASigner({ signer }),
kernelVersion: KERNEL_V3_1,
policies: [rootPolicy],
});
permissionValidator.address = contracts.executionDelayCorporatePolicy;
const rootValidatorId = concatHex([
VALIDATOR_TYPE.PERMISSION,
pad(permissionValidator.getIdentifier(), { size: 20, dir: 'right' }),
]);
const policyCallData = encodeFunctionData({
abi: KernelV3_1AccountAbi,
functionName: 'changeRootValidator',
args: [
rootValidatorId,
zeroAddress,
await permissionValidator.getEnableData(kernelAccount.address),
'0x',
],
});
// 2. Executor module
const executorCallData = encodeFunctionData({
abi: KernelV3_1AccountAbi,
functionName: 'installModule',
args: [BigInt(2), contracts.fundsManagement, concatHex([zeroAddress, zeroHash])],
});
// 3. Corporation creation
const createCorporationCallData = encodeFunctionData({
abi: CorporateAccountsAbi,
functionName: 'createCorporation',
args: [config.partnerId],
});
const batchedCalls = await kernelAccount.encodeCalls([
{ to: kernelAccount.address, value: BigInt(0), data: policyCallData },
{ to: kernelAccount.address, value: BigInt(0), data: executorCallData },
{ to: contracts.corporateAccounts, value: BigInt(0), data: createCorporationCallData },
]);
const opHash = await kernelClient.sendUserOperation({ callData: batchedCalls });
const receipt = await kernelClient.waitForUserOperationReceipt({ hash: opHash, timeout: 60_000 });
return {
corporationAddress: kernelAccount.address, // this is corporation_address in the API
ownerAddress: signer.address, // this is X-User-Wallet in the API
transactionHash: receipt.receipt.transactionHash,
};
}Next Steps
Once getCorporation returns a non-zero status, register the corporation through the API — see
Registering a Corporation.
Updated 20 days ago

