Collectable Publications

Learn the process of creating collectable Publications and the methods to collect them.


A collectable Publication is an on-chain Lens Publication that is configured with a Collect Action Module. This module, which implements the Open Action specification, allows users to mint new NFTs from a given Publication.

Collect Actions

The Lens Protocol includes two built-in Collect Action Modules:

  • Simple Collect Action Module

  • Multi-recipient Collect Action Module

The NFTs that are minted will reference the same Publication Metadata object as the original Publication. For more information on NFT-specific properties, refer to the Marketplace Metadata section.

This section assumes you are familiar with the Content Creation process. While we will use a Post to explain the concepts, the same principles apply to Comments and Quotes as well.

Simple Collect

The Simple Collect Action Module is one of the most versatile modules in the Lens Protocol. It enables you to create a collectable publication that could have one or more of the following features:

  • Only allows the author's followers to collect it

  • Requires collectors to pay a fee to collect

  • Specifies a percentage of the collect fee to be paid to referrers as a reward

  • Limits the total number of NFTs minted from the Publication

  • Enforces a time limit after which the Publication can no longer be collected

Below are some examples of the Simple Collect Action Module in action. You can mix and match the different options to create the collectable Publication that best suits your needs.

Limited Supply Collectable Available Only to Followers with an Expiry Date

import { OpenActionType, useCreatePost } from "@lens-protocol/react-web";
// ...
const { execute, loading, error } = useCreatePost();
// ...
const result = await execute({  metadata:    "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",  actions: [    {      type: OpenActionType.SIMPLE_COLLECT,      followerOnly: true,      collectLimit: 10, // max 10 collects      endsAt: new Date(2092, 0, 7), // collectable until 7th January 2092    },  ],});
// continue as usual

Collectable with Fee and Referral Reward

import { Amount, OpenActionType, useCreatePost } from "@lens-protocol/react-web";
// ...
const wmatic = ... // selected from useCurrencies hook
const { execute, loading, error } = useCreatePost();
// ...
const result = await execute({  metadata:    "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",  actions: [    {      type: OpenActionType.SIMPLE_COLLECT,       // 100 WMATIC      amount: Amount.erc20(wmatic, 100),      // 10% referral fee reward      referralFee: 10,      // if not specified, the author's owner address is used      recipient: '0x4f94FAFEE38F545920485fC747467EFc85C302E0',    },  ],});
// continue as usual

Multi-Recipient Collect

The Multi-recipient Collect Action Module provides the same features as the Simple Collect Action Module, but with the added ability to specify multiple recipients for the collect fee.

Given that the primary objective of this module is to divide the collect fee among multiple recipients, the fee amount is always required.

We will focus our examples on showcasing how to leverage the multi-recipient capability of this module.

import { Amount, OpenActionType, useCreatePost } from "@lens-protocol/react-web";
// ...
const wmatic = ... // selected from useCurrencies hook
const { execute, loading, error } = useCreatePost();
// ...
const result = await execute({  metadata:    "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",  actions: [    {      type: OpenActionType.MULTIRECIPIENT_COLLECT,       // 100 WMATIC      amount: Amount.erc20(wmatic, 100),      // 10% referral fee reward      referralFee: 10,      recipients: [        {          recipient: '0x4f94FAFEE38F545920485fC747467EFc85C302E0',          // 30% of collect fee after referral reward is paid          split: 30,        },        {          recipient: '0x097A4fE5cfFf0360438990b88549d4288748f6cB',          // 70% of collect fee after referral reward is paid          split: 70,        },      ],    },  ],});
// continue as usual

Collecting a Publication

Next, let's look at a collectable Publication from the perspective of a potential collector.

The examples below demonstrate this with a Post. However, the same principles apply to all Post, Comment, and Quote publications, regardless of the API request that returns them.

Read Collect Criteria

The initial step in implementing any Collect functionality is to understand the collect criteria of the target Publication. Such information can be used to inform the user about the requirements for collecting the Publication.

The resolveCollectPolicy helper function can be used to retrieve Collect Action Modules settings from the publication.openActionModules property and convert it into a developer-friendly CollectPolicy.

Available in @lens-protocol/react-web and @lens-protocol/react-native

import { PrimaryPublication, resolveCollectPolicy } from "@lens-protocol/react-web";
import { CollectFeeDetails } from "./CollectFeeDetails";
export function PublicationCollectPolicy({  publication,}: {  publication: PrimaryPublication;}) {  const policy = resolveCollectPolicy(publication);
  if (!policy) return null;
  return (    <div>      {policy.followerOnly === true && <p>Only followers can collect</p>}
      {policy.collectLimit && <p>{`Collect limit: ${policy.collectLimit}`}</p>}
      {policy.endsAt && <p>{`Ends at: ${policy.endsAt}`}</p>}
      {policy.fee === null && <p>Free collect</p>}
      {policy.fee && <CollectFeeDetails fee={policy.fee} />}    </div>  );}

Collecting with a Profile

If you're logged in with a Profile, you can collect a collectable Publication using a Sponsored Transaction and the Signless Experience, if it's enabled.

Once you've determined that the target Publication is collectable, you can proceed to collect it using the useOpenAction hook.

Available in @lens-protocol/react-web and @lens-protocol/react-native

This hook prioritizes the Signless Experience when available; otherwise, it resorts to a signed experience.

CollectButton.tsx
import { OpenActionKind, useOpenAction } from "@lens-protocol/react-web";
// ...
const { execute } = useOpenAction({  action: {    kind: OpenActionKind.COLLECT,  },});
// ...
const result = await execute({  publication: post,});
if (result.isFailure()) {  // error handling  return;}
// continue ...

Next, the Result<T, E> object returned by the execute callback can be used to differentiate between successful requests and potential failures.

In addition to the standard error handling, the useFollow hook can yield two additional errors:

  • InsufficientAllowanceError: This error occurs when the user's wallet has not approved the Collect Action Module contract to access the required collect fee amount.

  • InsufficientFundsError: This error occurs when the user's wallet does not have sufficient funds to pay the collect fee.

CollectButton.tsx
if (result.isFailure()) {  switch (result.error.name) {    case 'InsufficientAllowanceError':      window.alert(        'You must approve the contract to spend at least: +'          formatAmount(result.error.requestedAmount)      );      break;
    case 'InsufficientFundsError':      window.alert(        'You do not have enough funds to pay for this collect fee: '+          formatAmount(result.error.requestedAmount)      );      break;
    // handle other errors  }
  // eager return  return;}
// success ...

To enhance the user experience, you can manage the InsufficientAllowanceError by prompting the user to approve the underlying Collect Action Module contract to spend the necessary amount.

On the other hand, when the result is successful, the collect operation is optimistically reflected on the target Publication, affecting the following properties:

  • post.operations.canCollect: This value is set to TriStateValue.No while the transaction is being processed to prevent multiple concurrent collect attempts.

  • post.operations.hasCollected.value: This value is set to true.

  • post.operations.hasCollected.isFinalisedOnchain: This value is set to false.

  • post.stats.collects: This counter is incremented by one.

Optionally, you can monitor the transaction's status for the full completion of the collect operation using the result.value object.

CollectButton.tsx
const completion = await result.value.waitForCompletion();
// handle mining/indexing errorsif (completion.isFailure()) {  window.alert(completion.error.message);  return;}
window.alert("Collect operation finalized on-chain");

Regardless of whether you wait for completion, the post object will be updated at the end of the process as follows:

  • post.operations.canCollect: This value is set to TriStateValue.Yes.

  • post.operations.hasCollected.isFinalisedOnchain: This value will be set to true.

However, if the collect operation fails, these changes will be reverted.

In summary, a possible implementation of <CollectButton> would look like this:

CollectButton.tsx
import {  AnyPublication,  OpenActionKind,  useOpenAction,} from "@lens-protocol/react-web";
type CollectButtonProps = {  /**   * The Publication to collect  */  publication: AnyPublication;};
export function CollectButton(props: CollectButtonProps) {  const { execute, loading } = useOpenAction({    action: {      kind: OpenActionKind.COLLECT,    },  });
  const collect = async () => {    // execute the collect action    const result = await execute({      publication: props.publication,    });
    // handle relaying errors    if (result.isFailure()) {      window.alert(result.error.message);      return;    }
    window.alert("Publication collected");  };
  return (    <button onClick={collect} disabled={loading}>      Collect    </button>  );}

While only Post, Comment, and Quote, also known as Primary Publications, are directly collectable, the useOpenAction hook can be used with any Publication type, including Mirrors. This is why AnyPublication is used in the example above. The hook will automatically dereference and use the mirrored publication.

Collecting with a Wallet

You can also collect a collectable Publication using just a Wallet, provided you have authenticated with a Wallet-only flow.

Currently, collecting a publication with just a Wallet can only be done in a self-funded manner.

You should utilize the useOpenAction hook, but with the Self-Funded Transaction method.

The difference from the profile approach is minimal, so we will only highlight the relevant parts.

CollectButton.tsx
const { execute, loading } = useOpenAction({  action: {    kind: OpenActionKind.COLLECT,  },});
// ...
// execute the collect actionconst result = await execute({  publication: props.publication,  sponsored: false});
// continue as usual

We utilized the sponsored flag to force a direct contract call to the LensHub contract. This means that the user's wallet will cover the gas cost for the corresponding transaction.


That's it—you've just learned how to create collectable Publications, and how to collect them using a Profile or a Wallet.

Additional Options

ERC-20 Approvals

When collecting a Publication with a collect fee, an ERC-20 Approval may be required for the operation to succeed.

An ERC-20 Approval transaction doesn't affect the user's ERC20 balance. Instead, it authorizes the approved address (in this case, a Collect Action module contract) to withdraw the specified amount at a later time through an additional transaction.

You can perform an ERC-20 Approval for a Publication with a collect fee either upfront or in response to an InsufficientAllowanceError when attempting to collect the Publication.

Start by using the useApproveModule hook on the target publication object.

CollectButton.tsx
import { useApproveModule } from "@lens-protocol/react-web";
// ...
const approve = useApproveModule();
// ...
const result = await approve.execute({ on: publication });

Next, you can use the Result<T, E> object returned by the function to distinguish between successful requests and potential failures.

CollectButton.tsx
if (result.isFailure()) {  switch (result.error.name) {    case 'InsufficientGasError':      window.alert(        `The user's wallet does not have enough MATIC to pay for the transaction`      );      break;
    case 'TransactionError':      window.alert('There was an processing the transaction', result.error.message);      break;
    case 'PendingSigningRequestError':      window.alert(       'There is a pending signing request in your wallet. ' +         'Approve it or discard it and try again.'      );      break;
    case 'WalletConnectionError':      window.alert('There was an error connecting to your wallet', result.error.message);      break;
    case 'UserRejectedError':      // the user decided to not sign, usually this is silently ignored by UIs      break;  }
  // eager return  return;}
// continue with the collect operation

Upon a successful result, the ERC-20 Approval transaction is completed, and you can proceed with the collect operation, as shown in the following recap example.

CollectButton.tsx
import {  AnyPublication,  OpenActionKind,  useApproveModule,  useOpenAction,} from "@lens-protocol/react-web";
// ...
const { approve } = useApproveModule();const { collect } = useOpenAction({  action: {    kind: OpenActionKind.COLLECT,  },});
// ...
const approveCollectModuleFor = async (publication: AnyPublication) => {  const result = await approve({ on: publication });
  if (result.isFailure()) {    window.alert(result.error.message);    return;  }
  // try again the collect operation  return collect(profile);};
const collectPublication = async (publication: AnyPublication) => {  const result = await collect({ publication });
  if (result.isFailure()) {    switch (result.error.name) {      case "InsufficientAllowanceError":        return approveCollectModuleFor(publication);
      // other errors handling    }
    return;  }
  // successful collect  window.alert("You have collected this publication");};
// ...

By default, the useApproveModule hook executes an ERC-20 Approval for the exact amount required. However, you can pre-approve an unlimited amount by passing the limit argument:

CollectButton.tsx
import { useApproveModule, TokenAllowanceLimit } from "@lens-protocol/react-web";
// ...
const { execute, error, loading } = useApproveModule({  limit: TokenAllowanceLimit.INFINITE,});

That's it—you now have a complete flow for collect a Publication with a collect fee.