Connect to Lift
This guide connects an already-deployed Safe 1.4.1 account with Safe4337Module 0.3.0 to Lift. It does not deploy or reconfigure a Safe. Check the supported account configuration before proceeding.
1. Create an integration and policy
In the Lift console, select your workspace, create an integration for the intended project, and save the secret key when it is shown. You need an owner or admin role to manage integrations and policies.
Create an enabled policy for the integration with:
- Ethereum Sepolia (
11155111) selected. - Your deployed Safe's address in Allowed senders.
- The destination contract and its exact four-byte function selector in Permitted calls.
- Positive per-operation and daily budgets, with the daily budget at least the per-operation limit.
- An empty factory allowlist: the public pilot accepts deployed accounts only.
Copy the policy ID from Policies and the Sepolia bundler/paymaster endpoint from Integrations. The policy must belong to the integration that issued your key.
2. Configure your server
Set these variables in your server's secret configuration:
| Variable | Value |
|---|---|
LIFT_RPC_URL | The complete copied URL, ending in /api/lift/rpc/11155111 |
LIFT_API_KEY | The integration's lift_sk_… secret |
LIFT_POLICY_ID | The enabled policy ID |
SEPOLIA_RPC_URL | A separate execution RPC URL, such as a Sepolia endpoint copied from Gate |
Install the client packages:
bun add viem permissionlessLift uses the standard viem paymaster client and permissionless smart-account client. Account creation and signing remain your application's responsibility. If you use permissionless to load an existing Safe, supply its deployed address and the matching Safe4337 module configuration; see the permissionless Safe account reference.
3. Connect the existing account
The following server-side function takes the smart-account object you already use to sign for the supported Safe:
import { createPublicClient, http } from 'viem';
import { sepolia } from 'viem/chains';
import {
createPaymasterClient,
type SmartAccount,
} from 'viem/account-abstraction';
import { createSmartAccountClient } from 'permissionless';
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}`);
return value;
}
export function connectLift(deployedSafeAccount: SmartAccount) {
const publicClient = createPublicClient({
chain: sepolia,
transport: http(requiredEnv('SEPOLIA_RPC_URL')),
});
const liftTransport = http(requiredEnv('LIFT_RPC_URL'), {
fetchOptions: {
headers: {
Authorization: `Bearer ${requiredEnv('LIFT_API_KEY')}`,
},
},
});
return createSmartAccountClient({
account: deployedSafeAccount,
chain: sepolia,
client: publicClient,
bundlerTransport: liftTransport,
paymaster: createPaymasterClient({ transport: liftTransport }),
paymasterContext: { policyId: requiredEnv('LIFT_POLICY_ID') },
});
}The account must use the same chain and EntryPoint v0.7. Lift checks its deployed code and module configuration before authorizing sponsorship. A Safe7579 account object will not satisfy the current adapter.
4. Submit one permitted call
Use your application's account and a target authorized by the policy. This function demonstrates a single execute() call; replace the ABI and arguments with your contract's function:
import { encodeFunctionData, parseAbi, type Address } from 'viem';
import type { SmartAccount } from 'viem/account-abstraction';
import { connectLift } from './lift-client';
export async function executeWithLift(
deployedSafeAccount: SmartAccount,
allowedTarget: Address,
) {
const client = connectLift(deployedSafeAccount);
const data = encodeFunctionData({
abi: parseAbi(['function execute()']),
functionName: 'execute',
});
const transactionHash = await client.sendTransaction({
calls: [{ to: allowedTarget, data, value: 0n }],
});
return transactionHash;
}The example's target must implement execute(), and the policy must allow that function's selector. Send exactly one call with zero native value. A token contract call can still transfer token assets if its selector and account permissions allow it.
5. Confirm the outcome
Open Operations in Lift to find the authorization, UserOperation hash, transaction hash, and final charge. A UserOperation hash and its containing transaction hash are different identifiers.
For a minimal endpoint check that does not request sponsorship:
curl --fail-with-body "$LIFT_RPC_URL" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $LIFT_API_KEY" \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'An enabled Sepolia endpoint returns "0xaa36a7" as the result. Check the JSON body for an error even if HTTP status is 200. See API errors and operation recovery if sponsorship or submission fails.