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:

GuideWhy
Getting StartedIntegration overview
Credentialspartner_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:

  1. The corporation wallet is deployed.
  2. CorporateValidator is its root validator's signer module, ExecutionDelayCorporatePolicy is that
    validator's single policy, and the FundsManagement executor is installed.
  3. createCorporationForCorporateWallet(partnerId, wallet) has been called, creating the corporation
    record and assigning the creator the on-chain Owner role.

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 walletCorporation wallet
Registration contractAccountsCorporateAccounts
Registration callcreateUserAccountWithWallet(parentEntity)createCorporationForCorporateWallet(parentEntity, wallet)
Root validator signerECDSA signerCorporateValidator
Root validator policyExecutionDelayPolicyExecutionDelayCorporatePolicy
Executor moduleFundsManagementFundsManagement
Identity in the APIThe user's EOA (X-User-Wallet)The wallet address (corporation_address)

A wallet configured with the retail ExecutionDelayPolicy is refused with
Policy is not the execution delay policy. Resolve ExecutionDelayCorporatePolicy from the
ContractRegistry, not ExecutionDelayPolicy.


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:

OperationPermissions accepted
Account-change operations — installModule, changeRootValidator, installValidation, removeValidationsSu, AccountSu
Every other operationSu, 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;
ArgumentDescription
parentEntityYour partner_id, as bytes16
walletAddress 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.

RequirementDetail
Kernel v3.1 account deployedThe account address becomes corporation_address
FundsManagement installedModule type 2
Root validator is a permission validatorIts policyData must hold exactly one entry
That policy is ExecutionDelayCorporatePolicyResolved from the ContractRegistry
That validator's signer module is CorporateValidatorResolved from the ContractRegistry
CorporateValidator install databytes16 parentEntity ++ address creator, 36 bytes
The stored parentEntity matches the argumentOtherwise 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:

AddressAPI usage
Corporation wallet addresscorporation_address in the request body of register and login
Creator (EOA) addressX-User-Wallet header, and owner_address in GET /api/v1/corporations

Registration Checks

The call verifies, in order:

CheckRevert message
parentEntity is non-zeroInvalid parent entity
wallet is non-zeroInvalid wallet address
FundsManagement is installed as a type-2 moduleMissing executor module
The root validator resolves to exactly one policyPolicy data is not as it should be
That policy is ExecutionDelayCorporatePolicyPolicy is not the execution delay policy
The permission signer is CorporateValidatorSigner is not CorporateValidator
The validator's stored parent entity matchesParentEntity mismatch
The validator holds a creator addressCreator not set in validator
No corporation exists for this keyCorporation 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-chain

The returned struct:

FieldDescription
creatorThe owner address recorded at creation
status0 UNKNOWN, 1 PENDING, 2 ACTIVE, 3 BLOCKED, 4 DELETED
verificationStatus0 UNKNOWN, 1 APPLIED, 2 IN_REVIEW, 3 APPROVED, 4 CANCELLED, 5 REJECTED
walletStatus0 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.

RoleRole idOn-chain permissions
Ownered7a5845-8727-4b06-9ba0-7d43ddc9e6aaSu
Admin31eba1db-0809-4d44-a479-0b696c4a2603CardSu, EmployeeSu, TransactionSu, AccountSu
Treasuryce3de0cc-bd42-4bc8-829c-e395fab506efTransactionSu, CardSu, AccountSu
HRa1b2c3d4-e5f6-0718-29a0-b1c2d3e4f506EmployeeSu
Member7386b48f-df9c-49fe-8ccc-38b5b0b55932TransactionCreate, TransactionView, CardView, AccountView, EmployeeView
Viewer240d596c-8973-4203-827f-a2c994bceafaTransactionView, 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 encodes createCorporation(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() and signInPolicy() resolve
ExecutionDelayPolicy, not ExecutionDelayCorporatePolicy, 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;
ArgumentDescription
parentEntityYour 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:

createCorporationForCorporateWalletcreateCorporation — obsolete
Root validator signerCorporateValidatorA plain ECDSA signer
What validates a UserOperationThe signer's on-chain role in CorporateAccountsOne fixed key
Employees can sign for the corporationYes, per their roleNever
On-chain roles are consultedYesNo — the role records exist and are inert
Separation of dutiesRoles gate transactions and account changes independentlyNone

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.


Did this page help you?