Help & Support

Account Actions

This guide will explain how to use Account Actions and how to implement custom ones.

Account Actions are contracts that extend the Lens Protocol functionality by allowing Accounts to execute actions on other Accounts.

Lens includes a built-in TippingAccountAction, which is enabled by default and allows an Account to tip another Account.

Custom Account Actions can also be created but require explicit configuration.

Configuring Account Actions

Adding Account Actions

To configure an Account Action, follow these steps.

You MUST be authenticated as Account Owner or Account Manager of the Account you intend to configure an Account Action for.

1

Configure the Action

First, use the configureAccountAction action to configure a new Account Action.

Custom Account Action
import { blockchainData, evmAddress } from "@lens-protocol/client";import { configureAccountAction } from "@lens-protocol/client/actions";
const result = await configureAccountAction(sessionClient, {  action: {    unknown: {      address: evmAddress("0x1234…"),      params: [        {          raw: {            // 32 bytes key (e.g., keccak(name))            key: blockchainData("0xac5f04…"),            // an ABI encoded value            value: blockchainData("0x00"),          },        },      ],    },  },});

2

Handle Result

Then, handle the result using the adapter for the library of your choice:

import { handleOperationWith } from "@lens-protocol/client/viem";
// …
const result = await configureAccountAction(sessionClient, {  action: {    // …  },}).andThen(handleOperationWith(walletClient));

See the Transaction Lifecycle guide for more information on how to determine the status of the transaction.

Executing Account Actions

To execute an Account Action, follow these steps.

You MUST be authenticated as Account Owner or Account Manager to execute an Account Action.

1

Inspect Account Actions

First, inspect the target account.actions field to determine what Account Actions are available on it.

Account Actions
for (const action of account.actions) {  switch (action.__typename) {    case "TippingAccountAction":      // The Account has a Tipping Account Action enabled      break;
    case "UnknownAction":      // The Account has a Custom Account Action      break;  }}

See some examples below.

{  "__typename": "TippingAccountAction",  "address": "0x5678…" // the address to tip}

2

Execute Account Action

Next, execute the desired Account Action.

Use the executeAccountAction action to execute an Account Action.

import { evmAddress } from "@lens-protocol/client";import { executeAccountAction } from "@lens-protocol/client/actions";
const result = await executeAccountAction(sessionClient, {  account: evmAddress("0x1234…"),  action: {    tipping: {      currency: evmAddress("0x5678…"),      value: "0.42",    },  },});
if (result.isErr()) {  return console.error(result.error);}

3

Handle Result

Then, handle the result using the adapter for the library of your choice:

import { handleOperationWith } from "@lens-protocol/client/viem";
// …
const result = await executeAccountAction(sessionClient, {  account: evmAddress("0x1234…"),  action: {    // …  },}).andThen(handleOperationWith(walletClient));

See the Transaction Lifecycle guide for more information on how to determine the status of the transaction.

That's it—you've successfully executed an Account Action.

Building an Account Action

The Account Actions are defined by the IAccountAction interface, which basically requires three functions, one to configure the action, one to execute it, and another to disable it.

interface IAccountAction {    function configure(address originalMsgSender, address account, KeyValue[] calldata params)        external        returns (bytes memory);
    function execute(address originalMsgSender, address account, KeyValue[] calldata params)        external        returns (bytes memory);
    function setDisabled(address originalMsgSender, address account, bool isDisabled, KeyValue[] calldata params)        external        returns (bytes memory);}

But first, let's talk about the ActionHub

Before we dive into each of the functions from the IAccountAction interface, we need to talk about the ActionHub.

The ActionHub is a special contract that acts as the entry point for every Action in the Lens Protocol.

The purpose of that is to help discovery of Actions, having a single point where all main Action-related events are emitted.

So, each function of the IAccountAction interface must be only callable by ActionHub. For this, you can inherit BaseAccountAction contract, which acts as template for your custom Post Action, applying the restrictions needed, so you do not need to worry about it.

With this context, you can now understand why the first param of every IAccountAction function is originalMsgSender: this is the address that called the ActionHub originally, either to configure, execute, or disable an Action, given that msg.sender will always have the ActionHub address in the context of your Action contract.

Configuration

The configuration of the Action is done through the configure function, which purpose is to initialize any required state that the Action might require to work properly.

The function receives three parameters:

  • originalMsgSender: the address of the original msg.sender that invoked the ActionHub (as explained above).

  • account: the address of the Account for which the Action is configured for.

  • params: array of key-value pairs whose values can be decoded into any extra custom configuration parameters that the Action could require to work.

Return of the configure function is bytes - some Actions might want to return custom information to the caller.

This configure function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender).

For example, there might be Actions that do not require initialization at all, so the configure function implementation will be empty, while other Actions might require the caller to match the Account for which the actions is being configured for (i.e. being configured by the Account itself).

Keep in mind that if no prior configuration is required, the configure function must still be implemented and must not revert.

Every time the configure function is called, the ActionHub will emit a Lens_ActionHub_AccountAction_Configured event matching the parameters of the call (or Lens_ActionHub_AccountAction_Reconfigured if the configuration was updated).

Execution

The execution of the Action is done through the execute function, which purpose is to perform the actual action logic that the Action implements.

The function receives three parameters:

  • originalMsgSender: the address of the original msg.sender that invoked the ActionHub (as explained above).

  • account: the address of the Account for which the Action is being executed.

  • params: array of key-value pairs whose values can be decoded into any extra custom execution parameters that the Action could require to work.

Return of the execute function is bytes - some Actions might want to return custom information to the caller.

This execute function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender).

For example, there might be Actions that do not require any permissions at all, so the execute function implementation will be open, while other Actions might require the caller to match the some specific address.

Every time the execute function is called, the ActionHub will emit a Lens_ActionHub_AccountAction_Executed event matching the parameters of the call.

The ActionHub will not allow to invoke the execute function on an Account if the Action is disabled for it.

Disabling

The disabling of the Action is done through the setDisabled function, which purpose is to stop an Action to be executable for a given Account. The same function can be used to enable the Action back.

The function receives four parameters:

  • originalMsgSender: the address of the original msg.sender that invoked the ActionHub (as explained above).

  • account: the address of the Account for which the Action is being disabled/enabled.

  • isDisabled: boolean indicating if the Action is being disabled or enabled.

  • params: array of key-value pairs whose values can be decoded into any extra custom disabling/enabling parameters that the Action could require to work.

Return of the setDisabled function is bytes - some Actions might want to return custom information to the caller.

This setDisabled function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender).

Every time the setDisabled function is called, the ActionHub will emit a Lens_ActionHub_AccountAction_Disabled event matching the parameters of the call.

Example

Let's create an Pay-To-Repost Account Action that allows anyone to pay a fixed for the Account to repost a given Post.

1

Define State Variables

First, we define the necessary state variables. We need a mapping _prices to store the price configured by each account for a specific token. This same array can also act as an indicator if the action was configured (i.e. if the price is 0, it means the action was not configured for that account).

/** * @title SimplePayToReAccountAction * @notice A simple Account action allowing users to pay for the Account to repost a given post. */contract SimplePayToReAccountAction is BaseAccountAction, LensPaymentHandler {    // account => token => price    mapping(address => mapping(address => uint256)) private _prices;

2

Implement the Configure Function

Next, implement the _configure function. This function allows the account owner (originalMsgSender) to set the price for reposting using their account. It extracts the payment token and price from the params array.

It includes checks to ensure:

  1. Only the account owner can configure it (originalMsgSender == account).

  2. This action contract is set as an Account Manager for the target account, as it needs permission to execute the repost later.

  3. A valid token address and a price > 0 are provided.

// ... inside SimplePayToReAccountAction ...
    function _configure(        address originalMsgSender,        address account,        KeyValue[] calldata params    ) internal override returns (bytes memory) {        require(originalMsgSender == account, "Only account can configure");        require(IAccount(account).canExecuteTransactions(address(this)), "SimplePayToReAccountAction is not set as AccountManager");
        address token;        uint256 price = 0;        for (uint256 i = 0; i < params.length; i++) {          if (params[i].key == keccak256("lens.param.token")) {              token = abi.decode(params[i].value, (address));          } else if (params[i].key == keccak256("lens.param.price")) {              price = abi.decode(params[i].value, (uint256));          }        }
        require(token != address(0), "Token not found in params");        IERC20(token).balanceOf(address(this)); // Just checking if the token is valid        require(price > 0, "Valid price not found in params");
        _prices[account][token] = price;
        // Emitting an event Lens_ActionHub_AccountAction_Configured happens automatically via ActionHub        return "";    }

3

Implement the Execute Function

Implement the _execute function. This allows any user (originalMsgSender) to pay the configured price to make the account repost a specific post.

The function extracts the target feed, postId, payment token, and the expectedPrice from the params. It verifies that the expectedPrice matches the price configured by the account owner.

It then uses the inherited _handlePayment function (from LensPaymentHandler) to process the payment from the originalMsgSender to the account.

Finally, it executes the repost on behalf of the account by calling the repost function on the target IFeed interface. This requires the action contract to have been approved as an Account Manager during configuration.

// ... inside SimplePayToReAccountAction ...
    function _execute(        address originalMsgSender,        address account,        KeyValue[] calldata params    ) external override returns (bytes memory) {        address feed;        uint256 postId;        address token;        uint256 expectedPrice;        for (uint256 i = 0; i < params.length; i++) {          if (params[i].key == keccak256("lens.param.feed")) {              feed = abi.decode(params[i].value, (address));          } else if (params[i].key == keccak256("lens.param.postId")) {              postId = abi.decode(params[i].value, (uint256));          } else if (params[i].key == keccak256("lens.param.token")) {              token = abi.decode(params[i].value, (address));          } else if (params[i].key == keccak256("lens.param.expectedPrice")) {              expectedPrice = abi.decode(params[i].value, (uint256));          }        }        require(expectedPrice > 0, "Valid expected price not found in params");        require(_prices[account][token] == expectedPrice, "Not configured or wrong price expected");                // Function located at LensPaymentHandler contract:        _handlePayment({            payer: originalMsgSender,            token: token,            amount: expectedPrice,            recipient: account,            referrals: new RecipientData[](0),            referralFeeBps: 0        });                uint256 repostId = IFeed(feed).repost({          postParams: CreatePostParams({              author: account,              contentURI: "",              repostedPostId: postId,              quotedPostId: 0,              repliedPostId: 0,              ruleChanges: new RuleChange[](0),              extraData: new KeyValue[](0)          }),          customParams: new KeyValue[](0),          feedRulesParams: new RuleProcessingParams[](0),          rootPostRulesParams: new RuleProcessingParams[](0),          quotedPostRulesParams: new RuleProcessingParams[](0),        });
        return abi.encode(repostId);    }

After adding the corresponding imports, our final code should look something like this:

// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
import { KeyValue } from "contracts/core/Types.sol";import { BaseAccountAction } from "contracts/actions/account/base/BaseAccountAction.sol";import { IFeed } from "contracts/core/interfaces/IFeed.sol";import { LensPaymentHandler } from "contracts/extensions/fees/LensPaymentHandler.sol";
/** * @title SimplePayToReAccountAction * @notice A simple Account action allowing users to pay for the Account to repost a given post. */contract SimplePayToReAccountAction is BaseAccountAction, LensPaymentHandler {    mapping(address account => mapping(address token => uint256 price))) private _prices;
    /**      * @notice Configures the SimplePayToReAccount Action for a given post.      * @param originalMsgSender The address initiating the configuration via the ActionHub. Must be post the account.      * @param account The address of the account that configures the Pay-To-Repost for itself.      * @param params KeyValue array containing the token and price configuration parameters.      * @return bytes abi.encoded(token, price).     */    function _configure(        address originalMsgSender,        address account,        KeyValue[] calldata params    ) internal override returns (bytes memory) {        require(originalMsgSender == account, "Only account can configure");        require(IAccount(account).canExecuteTransactions(address(this)), "SimplePayToReAccountAction is not set as AccountManager");
        address token;        uint256 price = 0;        for (uint256 i = 0; i < params.length; i++) {          if (params[i].key == keccak256("lens.param.token")) {              token = abi.decode(params[i].value, (address));          } else if (params[i].key == keccak256("lens.param.price")) {              price = abi.decode(params[i].value, (uint256));          }        }
        require(token != address(0), "Token not found in params");        IERC20(token).balanceOf(address(this)); // Just checking if the token is valid        require(price > 0, "Valid price not found in params");
        _prices[account][token] = price;
        // Emitting an event Lens_ActionHub_AccountAction_Configured happens automatically via ActionHub and params        // already contain token & price, so we don't have to encode them again for emitting.        return "";    }
    /**     * @notice Executes a repost of a given post by a configured Account.     * @param originalMsgSender The address initiating the repost via the ActionHub.     * @param account The address of the account where the Pay-To-Repost Action was configured.     * @param params Array of key-value pairs. Expected to contain the feed and postId to repost.     * @return bytes feed and post.     * Requirements:     * - The `originalMsgSender` must not have voted on this `postId` before.     * - `params` must not be empty and the first element's value must be abi-decodable as a boolean.     */    function _execute(        address originalMsgSender,        address account,        KeyValue[] calldata params    ) external override returns (bytes memory) {        address feed;        uint256 postId;        address token;        uint256 expectedPrice;        for (uint256 i = 0; i < params.length; i++) {          if (params[i].key == keccak256("lens.param.feed")) {              feed = abi.decode(params[i].value, (address));          } else if (params[i].key == keccak256("lens.param.postId")) {              postId = abi.decode(params[i].value, (uint256));          } else if (params[i].key == keccak256("lens.param.token")) {              token = abi.decode(params[i].value, (address));          } else if (params[i].key == keccak256("lens.param.expectedPrice")) {              expectedPrice = abi.decode(params[i].value, (uint256));          }        }        require(expectedPrice > 0, "Valid expected price not found in params");        require(_prices[account][token] == expectedPrice, "Not configured or wrong price expected");                // Function located at LensPaymentHandler contract:        _handlePayment({            payer: originalMsgSender,            token: token,            amount: expectedPrice,            recipient: account,            referrals: new RecipientData[](0),            referralFeeBps: 0        });                uint256 repostId = IFeed(feed).repost({          postParams: CreatePostParams({              author: account,              contentURI: "",              repostedPostId: postId,              quotedPostId: 0,              repliedPostId: 0,              ruleChanges: new RuleChange[](0),              extraData: new KeyValue[](0)          }),          customParams: new KeyValue[](0),          feedRulesParams: new RuleProcessingParams[](0),          rootPostRulesParams: new RuleProcessingParams[](0),          quotedPostRulesParams: new RuleProcessingParams[](0),        });
        return abi.encode(repostId);    }}