Skip to main content

· 16 min read
Fangting
Raymond Feng

Introduction

We're excited to share Collab.Land is partnering with Alchemy to advance Account Abstraction(AA) and modular plugins(ERC-6900). This in-depth guide explains how to create adaptable ERC-6900 plugins, a core component of AA. Stay tuned as we work together to bring these cutting-edge features to our products.

You have decided to write an ERC-6900 plugin that can be used by all ERC-6900 compliant accounts. That’s great!

But, where do you start?

You have come to the right place! In this guide, we will walk through how to write an ERC-6900 plugin by writing a multi-owner plugin together in Solidity.

Terminology

Here is a list of ERC-6900 terminology that we will reference throughout the article:

  • MSCA: modular smart contract account that is compliant with ERC-6900.
  • User Operation: an ERC-4337 term that is often used to indicate the context for an ERC-4337 transaction format where txs are sent to the EntryPoint contract first, who then calls accounts with user operation(s).
  • Runtime: a term used to indicate the traditional transaction context, as opposed to the above ERC-4337 transaction format. Here, txs are sent directly to the smart contract, rather than from the EntryPoint contract via user operations.

Resources

An ERC-6900 plugin needs to implement required interfaces and abide by ERC-6900 rules for plugins. The reference implementation of ERC-6900 provides a nice BasePlugin and IPlugin, as well as plugin examples. Those are all good materials to help you understand how to write a plugin.

We will also release a set of dev tools in the coming weeks to make it extremely easy to write a plugin. Stay tuned!

When you are ready, let’s get started.

1. What is an ERC-6900 plugin?

An ERC-6900 plugin is a smart contract that provides up to three types of functions for an MSCA aside from its own functions.

  • Validation functions. Functions that validate authentication and authorization for an MSCA.
  • Execution functions. Functions that define the main execution logic of a function of an MSCA.
  • Hook functions. Functions can be run before the above Validation functions, or before and after the above Execution functions, for whatever purposes.

An ERC-6900 plugin can be installed by one or more MSCAs of different implementations that are compliant with ERC-6900. It stores states of those MSCAs and provides intended functionalities. For security reasons, plugins should be deployed as a global singleton rather than an upgradable proxy to ensure security and self-custody for MSCA to use.

2. What does the plugin do?

To write a plugin for any MSCA, we first need to define the purpose of the plugin by asking a few key questions. We will use the Multi-Owner Plugin we are writing here to answer the questions.

What does the plugin do?

The Multi-Owner plugin manages owners of an MSCA. It allows multiple owners of an MSCA to authorize executions.

What kind of functions and logic does the plugin have?

The Multi-Owner plugin will be able to:

  • Update (add, delete) owners for an MSCA.
  • Check if an address is an owner of an MSCA.
  • Show all owners of an MSCA.
  • Validate owner signatures of ERC-4337 enabled UserOperation transactions as well as runtime transactions.

What kind of extra standards/interfaces does the plugin support?

The Multi-Owner plugin will support ERC-1271 for signature validation.

3. Map Plugin Logic to MSCA Functions

We can now map the logic in step 2 into the step 1’s function types of a plugin.

  • Update (add, delete) owners for an MSCA ⇒ updateOwners ⇒ Execution Function
  • Check if an address is an owner of an MSCA ⇒ isOwner ⇒ Execution Function (optional)
  • Show all owners of an MSCA ⇒ owners ⇒ Execution Function (optional)
  • Validate owner signatures ⇒ validate ⇒ Validation Function
  • Support ERC-1271 standard ⇒ supportsInterface ⇒ Plugin Function
  • Support ERC-1271 signature validation ⇒ isValidSignature ⇒ Execution Function

You will notice some of the execution functions are optional. Those are view functions. Viewing state of an MSCA can be through the MSCA itself (by installing those functions as execution functions on the account) or can be through the plugin. Therefore, it is up to the plugin dev to decide if it is beneficial to install view functions. The tradeoff is installation gas cost vs convenient access to states through MSCA itself.

In this Multi-Owner plugin, we will not write any hook function. One can follow the same process to map hook functions.

We have a pretty good structure of the Multi-Owner plugin now. It is time to put it in code.

4. Write Plugin Logic

Based on the IPlugin interface of ERC-6900, we will use the ERC-6900 reference implementation’s BasePlugin as our base.

The core logic might look like the following:

   contract MultiOwnerPlugin is BasePlugin, IERC1271 {
function updateOwners(address[] memory ownersToAdd, address[] memory ownersToRemove)
public
{} // write your logic here

function isOwner(address addrToCheck) external view returns (bool) {}
function owners() external view returns (address[] memory) {}

/// @inheritdoc IERC1271
/// @dev The signature is valid if it is signed by one of the owners' private key
/// (if the owner is an EOA) or if it is a valid ERC-1271 signature from one of the
/// owners (if the owner is a contract). Note that unlike the signature
/// validation used in `validateUserOp`, this does not wrap the digest in
/// an "Ethereum Signed Message" envelope before checking the signature in
/// the EOA-owner case.
function isValidSignature(bytes32 digest, bytes memory signature) public view override returns (bytes4) {}

/// @inheritdoc BasePlugin
/// @notice Run the user operation validationFunction specified by the `functionId`.
/// @param functionId An identifier that routes the call to different internal implementations, should there be
/// more than one.
/// @param userOp The user operation.
/// @param userOpHash The user operation hash.
/// @return Packed validation data for validAfter (6 bytes), validUntil (6 bytes), and authorizer (20 bytes).
function userOpValidationFunction(uint8 functionId, UserOperation calldata userOp, bytes32 userOpHash)
external
view
override
returns (uint256)
{} // write your user operation vaidation logic here, for ERC-4337 call path

/// @inheritdoc BasePlugin
/// @notice Run the runtime validationFunction specified by the `functionId`.
/// @dev To indicate the entire call should revert, the function MUST revert.
/// @param functionId An identifier that routes the call to different internal implementations, should there be
/// more than one.
/// @param sender The caller address.
/// @param value The call value.
/// @param data The calldata sent.
function runtimeValidationFunction(uint8 functionId, address sender, uint256 value, bytes calldata data)
external
virtual
{} // write your user operation vaidation logic here, for non-ERC-4337 call path

/// @inheritdoc BasePlugin
/// @dev Returns true if this contract implements the interface defined by
/// `interfaceId`. See the corresponding
/// https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
/// to learn more about how these ids are created.
///
/// This function call must use less than 30 000 gas.
///
/// Supporting the IPlugin interface is a requirement for plugin installation. This is also used
/// by the modular account to prevent standard execution functions `execute` and `executeBatch` from
/// making calls to plugins.
/// @param interfaceId The interface ID to check for support.
/// @return True if the contract supports `interfaceId`.
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IPlugin).interfaceId || super.supportsInterface(interfaceId);
}
}

You will notice most of the functions are fixed name methods required for the type of functions from ERC-6900, and ERC-1271:

ERC-1271:

  • isValidSignature

ERC-6900:

  • userOpValidationFunction
  • runtimeValidationFunction
  • supportsInterface

The rest of the functions are defined by plugin authors:

  • updateOwners
  • isOwner
  • owners

5. MSCA Installation & Uninstallation

Since we are building a global singleton plugin, assuming there will be more than one MSCA using our plugin, it is time to think how we store MSCA states/data and handle the installation and uninstallation process.

Plugin Storage Consideration

To support ERC-4337 storage access rules (you can only access MSCA address associated storage slots during the user operation validation phase), MSCA data needs to to be accessed only through MSCA address.

In this Multi-Owner plugin, we will need to store a collection of owners for each MSCA in the form of one of the following:

  • map of arrays: mapping(address => address[])
  • map of mapping: mapping(address => mapping(address => bool)))

Based on how Solidity stores nested dynamic data types, we won’t able to store the above directly on the plugin contract without violating the ERC-4337 storage access rules.

As our plugin will be a global singleton, we need to find a way to use an MSCA account address to construct a key to store associated data/list (aka owner list) without breaking the storage access rules of ERC-4337.

Good news! We will have audited libraries (to be released soon) to help.

At this post, we will use an AssociatedLinkedListSet of bytes32, and AssociatedLinkedListSetLib from ERC-6900 reference implementation to help store the owners for our plugin.

It might look like this in code:

contract MultiOwnerPlugin is BasePlugin, IERC1271 {
AssociatedLinkedListSet internal _owners;
}

Installation Consideration

When an MSCA installs a global singleton plugin, the plugin needs to initialize necessary states for the MSCA and install necessary execution functions on to the MSCA.

All those behaviors are defined in the following two functions as part of the IPlugin interfaces that ERC-6900 requires all plugins to implement.

    /// @notice Initialize plugin data for the modular account.
/// @dev Called by the modular account during `installPlugin`.
/// @param data Optional bytes array to be decoded and used by the plugin to setup initial plugin data for the
/// modular account.
function onInstall(bytes calldata data) external;

/// @notice Describe the contents and intended configuration of the plugin.
/// @dev This manifest MUST stay constant over time.
/// @return A manifest describing the contents and intended configuration of the plugin.
function pluginManifest() external pure returns (PluginManifest memory);

In this Multi-Owner plugin, we want to initialize owners for the MSCA. It might look like this:

function onInstall(bytes calldata data) external override isNotInitialized(msg.sender) {
(address[] memory initialOwners) = abi.decode(data, (address[]));

// require non empty owner list
if (initialOwners.length == 0) {
revert EmptyOwnersNotAllowed();
}
address associated = msg.sender; // the associated storage for MSCA (mag.sender)
uint256 length = initialOwners.length;
for (uint256 i = 0; i < length;) {
if (!_owners.tryAdd(associated, SetValue.wrap(bytes30(bytes20(initialOwners[i]))))) {
// owner cannot be address(0) or duplicated
revert InvalidOwner(initialOwners[i]);
}

unchecked {
++i;
}
}
}

We specify what to be installed on the MSCA through the pluginManifest method. Based on the previous steps on business logic and plugin mapping. We have the following:

  • Install 4 identified execution functions (including 2 optional view functions) from above.
  • Apply user operations validation function to some MSCA’s native functions and this plugin’s execution function (6 in total and state changing functions only).
    • MSCA’s native functions: execute, executeBatch, installPlugin, uninstallPlugin, upgradeToAndCall. Once installed, all those native functions can be called with owner signature only. Note: this is not necessary for almost any other plugins but plugins that manage ownership.
    • Plugin’s execution function: updateOwners. We don’t need to apply the user operation validation function for the two optional view functions. View functions should never be access through the user operation call path as it will cost MSCA gas. It should always be accessed through the runtime call path.
  • Apply runtime validation function.
    • Apply the runtimeValidationFunction to the same functions (6 in total) as the above point.
    • Indicate all view only execution functions (3 total, including the two optional view functions) should always be allowed in runtime. View functions should only be allowed in the runtime call path.

Code Example for Multi-Owner Plugin

  /// @inheritdoc BasePlugin
function pluginManifest() external pure override returns (PluginManifest memory) {
PluginManifest memory manifest;

manifest.executionFunctions = new bytes4[](4);
manifest.executionFunctions[0] = this.updateOwners.selector;
manifest.executionFunctions[1] = this.owners.selector;
manifest.executionFunctions[2] = this.isOwner.selector;
manifest.executionFunctions[3] = this.isValidSignature.selector;

ManifestFunction memory ownerUserOpValidationFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.SELF,
functionId: uint8(FunctionId.USER_OP_VALIDATION_OWNER),
dependencyIndex: 0 // Unused.
});

// Update Modular Account's native functions to use userOpValidationFunction provided by this plugin
manifest.userOpValidationFunctions = new ManifestAssociatedFunction[](6);
manifest.userOpValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.updateOwners.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[1] = ManifestAssociatedFunction({
executionSelector: IStandardExecutor.execute.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[2] = ManifestAssociatedFunction({
executionSelector: IStandardExecutor.executeBatch.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[3] = ManifestAssociatedFunction({
executionSelector: UpgradeableModularAccount.installPlugin.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[4] = ManifestAssociatedFunction({
executionSelector: UpgradeableModularAccount.uninstallPlugin.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[5] = ManifestAssociatedFunction({
executionSelector: UUPSUpgradeable.upgradeToAndCall.selector,
associatedFunction: ownerUserOpValidationFunction
});

ManifestFunction memory ownerOrSelfRuntimeValidationFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.SELF,
functionId: uint8(FunctionId.RUNTIME_VALIDATION_OWNER_OR_SELF),
dependencyIndex: 0 // Unused.
});
ManifestFunction memory alwaysAllowFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW,
functionId: 0, // Unused.
dependencyIndex: 0 // Unused.
});

// Update Modular Account's native functions to use runtimeValidationFunction provided by this plugin
manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](9);
manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.updateOwners.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[1] = ManifestAssociatedFunction({
executionSelector: IStandardExecutor.execute.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[2] = ManifestAssociatedFunction({
executionSelector: IStandardExecutor.executeBatch.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[3] = ManifestAssociatedFunction({
executionSelector: UpgradeableModularAccount.installPlugin.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[4] = ManifestAssociatedFunction({
executionSelector: UpgradeableModularAccount.uninstallPlugin.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[5] = ManifestAssociatedFunction({
executionSelector: UUPSUpgradeable.upgradeToAndCall.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[6] = ManifestAssociatedFunction({
executionSelector: this.isValidSignature.selector,
associatedFunction: alwaysAllowFunction
});
manifest.runtimeValidationFunctions[7] = ManifestAssociatedFunction({
executionSelector: this.isOwner.selector,
associatedFunction: alwaysAllowFunction
});
manifest.runtimeValidationFunctions[8] = ManifestAssociatedFunction({
executionSelector: this.owners.selector,
associatedFunction: alwaysAllowFunction
});

return manifest;
}

Uninstallation Consideration

When an MSCA uninstalls a global singleton plugin, the plugin needs to handle clearing states for the MSCA. The behavior is defined in the following function:

    /// @notice Clear plugin data for the modular account.
/// @dev Called by the modular account during `uninstallPlugin`.
/// @param data Optional bytes array to be decoded and used by the plugin to clear plugin data for the modular
/// account.
function onUninstall(bytes calldata data) external;

Initializing Owners for the MSCA

In this Multi-Owner plugin example, we want to initialize owners for the MSCA.

It might look like the following:

  /// @inheritdoc BasePlugin
function onUninstall(bytes calldata) external override {
_owners.clear(msg.sender);
}

6. Plugin Metadata Setup

It’s time to setup the plugin’s metadata to help users understand what the plugin does. The metadata provides authorship, version, and permission information.

For the Multi-Owner plugin, it might look like this:

    /// @inheritdoc BasePlugin
function pluginMetadata() external pure virtual override returns (PluginMetadata memory) {
PluginMetadata memory metadata;
metadata.name = "Multi Owner Plugin";
metadata.version = "1.0.0";
metadata.author = "ERC-6900 Plugin Tutorial";

// Permission strings
string memory modifyOwnershipPermission = "Modify Ownership";

// Permission descriptions
metadata.permissionDescriptors = new SelectorPermission[](1);
metadata.permissionDescriptors[0] = SelectorPermission({
functionSelector: this.updateOwners.selector,
permissionDescription: modifyOwnershipPermission
});

return metadata;
}

7. Events and Errors

Events and errors are essential to help users and off-chain services/clients to understand what is going on with a certain action. When writing a plugin, we should ask ourselves:

  • Do I have clear errors when things go wrong?
  • Do I emit enough data to help users and services to build the history state of an MSCA?

8. Putting it All Together

Here is what the Multi-Owner plugin code looks like altogether:

pragma solidity ^0.8.21;

import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import {UserOperation} from "@eth-infinitism/account-abstraction/interfaces/UserOperation.sol";

import {BasePlugin} from "@erc6900/reference-implementation/src/plugins/BasePlugin.sol";
import {
ManifestAssociatedFunction,
ManifestAssociatedFunctionType,
ManifestFunction,
PluginManifest,
PluginMetadata,
SelectorPermission
} from "@erc6900/reference-implementation/src/interfaces/IPlugin.sol";
import {IStandardExecutor} from "@erc6900/reference-implementation/src/interfaces/IStandardExecutor.sol";
import {IStandardExecutor} from "@erc6900/reference-implementation/src/libraries/AssociatedLinkedListSetLib.sol";

/// @title Multi Owner Plugin
/// @author ERC-6900 Plugin Tutorial
contract MultiOwnerPlugin is BasePlugin, IERC1271 {
using AssociatedLinkedListSetLib for AssociatedLinkedListSet;

string internal constant _NAME = "Multi Owner Plugin";
string internal constant _VERSION = "1.0.0";
string internal constant _AUTHOR = "ERC-6900 Plugin Tutorial";

constructor() {}

AssociatedLinkedListSet internal _owners;

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Execution functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

function updateOwners(address[] memory ownersToAdd, address[] memory ownersToRemove)
public
isInitialized(msg.sender)
{ // update MSCA owners logic lives here}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Execution view functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

function isOwner(address ownerToCheck) external view returns (bool) {
return isOwnerOf(msg.sender, ownerToCheck);
}

function owners() external view returns (address[] memory) {
return ownersOf(msg.sender);
}

/// @inheritdoc IERC1271
/// @dev The signature is valid if it is signed by one of the owners' private key
/// (if the owner is an EOA) or if it is a valid ERC-1271 signature from one of the
/// owners (if the owner is a contract). Note that unlike the signature
/// validation used in `validateUserOp`, this does not wrap the digest in
/// an "Ethereum Signed Message" envelope before checking the signature in
/// the EOA-owner case.
function isValidSignature(bytes32 digest, bytes memory signature) public view override returns (bytes4) {
// ERC-1271 signature validation logic lives here
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin interface functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

/// @inheritdoc BasePlugin
function _onInstall(bytes calldata data) internal override isNotInitialized(msg.sender) {
(address[] memory initialOwners) = abi.decode(data, (address[]));

// require non empty owner list
if (initialOwners.length == 0) {
revert EmptyOwnersNotAllowed();
}
address associated = msg.sender; // the associated storage for MSCA (mag.sender)
uint256 length = initialOwners.length;
for (uint256 i = 0; i < length;) {
if (!_owners.tryAdd(associated, SetValue.wrap(bytes30(bytes20(initialOwners[i]))))) {
revert InvalidOwner(initialOwners[i]);
}

unchecked {
++i;
}
}
}

/// @inheritdoc BasePlugin
function onUninstall(bytes calldata) external override {
_owners.clear(msg.sender);
}

/// @inheritdoc BasePlugin
function userOpValidationFunction(uint8 functionId, UserOperation calldata userOp, bytes32 userOpHash)
external
view
override
returns (uint256)
{ // user operation validation logic lives here }

/// @inheritdoc BasePlugin
function runtimeValidationFunction(uint8 functionId, address sender, uint256, bytes calldata)
external
view
override
{ // runtime validation logic lives here }

/// @inheritdoc BasePlugin
function pluginManifest() external pure override returns (PluginManifest memory) {
PluginManifest memory manifest;

manifest.executionFunctions = new bytes4[](4);
manifest.executionFunctions[0] = this.updateOwners.selector;
manifest.executionFunctions[1] = this.owners.selector;
manifest.executionFunctions[2] = this.isOwner.selector;
manifest.executionFunctions[3] = this.isValidSignature.selector;

ManifestFunction memory ownerUserOpValidationFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.SELF,
functionId: uint8(FunctionId.USER_OP_VALIDATION_OWNER),
dependencyIndex: 0 // Unused.
});

// Update Modular Account's native functions to use userOpValidationFunction provided by this plugin
manifest.userOpValidationFunctions = new ManifestAssociatedFunction[](6);
manifest.userOpValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.updateOwners.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[1] = ManifestAssociatedFunction({
executionSelector: IStandardExecutor.execute.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[2] = ManifestAssociatedFunction({
executionSelector: IStandardExecutor.executeBatch.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[3] = ManifestAssociatedFunction({
executionSelector: UpgradeableModularAccount.installPlugin.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[4] = ManifestAssociatedFunction({
executionSelector: UpgradeableModularAccount.uninstallPlugin.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[5] = ManifestAssociatedFunction({
executionSelector: UUPSUpgradeable.upgradeToAndCall.selector,
associatedFunction: ownerUserOpValidationFunction
});

ManifestFunction memory ownerOrSelfRuntimeValidationFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.SELF,
functionId: uint8(FunctionId.RUNTIME_VALIDATION_OWNER_OR_SELF),
dependencyIndex: 0 // Unused.
});
ManifestFunction memory alwaysAllowFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW,
functionId: 0, // Unused.
dependencyIndex: 0 // Unused.
});

// Update Modular Account's native functions to use runtimeValidationFunction provided by this plugin
manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](9);
manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.updateOwners.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[1] = ManifestAssociatedFunction({
executionSelector: IStandardExecutor.execute.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[2] = ManifestAssociatedFunction({
executionSelector: IStandardExecutor.executeBatch.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[3] = ManifestAssociatedFunction({
executionSelector: UpgradeableModularAccount.installPlugin.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[4] = ManifestAssociatedFunction({
executionSelector: UpgradeableModularAccount.uninstallPlugin.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[5] = ManifestAssociatedFunction({
executionSelector: UUPSUpgradeable.upgradeToAndCall.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[6] = ManifestAssociatedFunction({
executionSelector: this.isValidSignature.selector,
associatedFunction: alwaysAllowFunction
});
manifest.runtimeValidationFunctions[7] = ManifestAssociatedFunction({
executionSelector: this.isOwner.selector,
associatedFunction: alwaysAllowFunction
});
manifest.runtimeValidationFunctions[8] = ManifestAssociatedFunction({
executionSelector: this.owners.selector,
associatedFunction: alwaysAllowFunction
});

return manifest;
}

/// @inheritdoc BasePlugin
function pluginMetadata() external pure virtual override returns (PluginMetadata memory) {
PluginMetadata memory metadata;
metadata.name = _NAME;
metadata.version = _VERSION;
metadata.author = _AUTHOR;

// Permission strings
string memory modifyOwnershipPermission = "Modify Ownership";

// Permission descriptions
metadata.permissionDescriptors = new SelectorPermission[](1);
metadata.permissionDescriptors[0] = SelectorPermission({
functionSelector: this.updateOwners.selector,
permissionDescription: modifyOwnershipPermission
});

return metadata;
}

// ┏━━━━━━━━━━━━━━━┓
// ┃ EIP-165 ┃
// ┗━━━━━━━━━━━━━━━┛

/// @inheritdoc BasePlugin
function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
return interfaceId == type(IMultiOwnerPlugin).interfaceId || super.supportsInterface(interfaceId);
}
}

More Resources

To learn more about the ERC-6900 ecosystem, check out the following resources:

· 3 min read
Ekene Eze

A few months ago we announced the Login with Collab.Land Flow. An effort that allows Collab.Land developers to securely authenticate users and gain access to their Collab.Land resources. By registering a client application through the developer portal, developers can obtain credentials that enable them to interact with the Collab.Land API on behalf of authenticated users.

This has been very well received with many adoption instances and use cases emerging within our online communities. For community administrators, this represents a great opportunity to create online resources exclusively accessible to their community members, all without the need for extensive development work.

The success of this feature and the requests from our developers have led us to an exciting decision: we are now making this functionality available for miniapps as well. This means that miniapp developers can prompt Discord users to grant authorization for accessing their data, such as connected wallets and profiles.

💡 If you've ever envisioned building a miniapp that relies on users granting access to their wallet addresses, this feature now makes it a reality.

How it works

Collab.Land miniapps are built using Collab Actions, which are pre-built templates designed to streamline the miniapp creation process. These templates come with essential functionalities like signature verification built-in. The hello-action example serves as a prime example of how Collab Actions operate.

When you run the Action locally, users can execute a slash command and provide a name as a parameter. The Action sends this provided name to Collab.Land, which processes the request and responds with a greeting.

What we've done to enhance this flow is to introduce an additional step that enables you (the developer) to request user authorization for data access. To implement this feature, we've added two functions behind the scenes: one to request user permission and another to construct the modal for user authorization.

The requestUserPermissions function is responsible for requesting user permission to access their data:

async requestUserPermissions(
request: DiscordActionRequest<APIChatInputApplicationCommandInteraction>,
permissions: string[],
) {
const metadata = await this.getMetadata();
if (metadata.manifest.clientId == null) {
throw new HttpErrors.BadRequest(`Missing clientId in manifest`);
}
const msg = this.buildUserPermissionMessage(
request.id,
metadata.manifest.clientId,
permissions,
);

return this.followupMessage(request, msg);
}

This function takes a DiscordActionRequest object and an array of permissions that you wish to request from the user. It then constructs a message requesting permission and returns it using the followupMessage function.

The second function, buildUserPermissionMessage, creates the modal that solicits user authorization. It takes the request ID, your miniapp's client ID, and an array of permissions you want to request from the user:

buildUserPermissionMessage(
interactionId: string,
clientId: string,
scopes: string[],
) {
const builder = new EmbedBuilder()
.setTitle('Request user permissions')
.addFields(
{
name: ACTION_CLIENT_ID,
value: clientId,
},
{
name: ACTION_REQUESTED_SCOPES,
value: scopes.join(' '),
},
)
.setDescription(
`Action ${clientId} requires the following permissions:\n` +
scopes.map(p => `- **${p}**`).join('\n'),
);
const row =
new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`${APPROVE_USER_PERMISSIONS}:${interactionId}`)
.setLabel(`Approve`)
.setEmoji({
name: '✅',
})
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`${DENY_USER_PERMISSIONS}:${interactionId}`)
.setLabel(`Deny`)
.setEmoji({
name: '❌',
})
.setStyle(ButtonStyle.Primary),
);
const msg: RESTPostAPIWebhookWithTokenJSONBody = {
embeds: [builder.toJSON()],
components: [row.toJSON()],
};
return msg;
}

The modal contains a message explaining what data you want access to. Once the user grants permission, you can use the Collab.Land API to access their data and build your miniapp accordingly.

All of this functionality is already integrated into the hello-action example repository, ready for you to clone and explore as you see fit. With these recent updates, you can now create miniapps that offer user-specific interactions, access user profiles, connected wallets, and communities.

Get started from the repo here.

· 8 min read
Ekene Eze

Security is a top priority for us here at Collab.Land. We not only ensure the safety of our members and admins but also take great care in securing the connections and data shared by developers through our APIs. One essential aspect of this security is signature verification. Today, we'll explore the importance of signature verification and guide you through the process of implementing it in your Miniapps on Collab.Land.

Why Signature Verification Matters

Imagine sending a valuable package through the mail. To ensure it arrives intact and untampered with, you'd use a sealed envelope. In the digital world, that envelope is a digital signature. It guarantees the authenticity and integrity of your data in an environment where anyone can intercept and manipulate it if not properly secured.

Digital signatures involve two fundamental operations:

  1. Signing: Using a private key to produce a digital signature for raw data.
  2. Verification: Using a public key to confirm the authenticity and integrity of data against the received raw data.

Web2 and Web3 technologies heavily rely on digital signatures to preserve data integrity and protection, but they serve slightly different purposes.

Web2: Data Integrity and Security

In Web2, digital signatures are commonly used to verify data integrity and secure open API endpoints from common denial-of-service attacks. They provide integrity, authentication, and non-repudiation. We see this commonly used in email authentication, WhatsApp, Imessage, etc where a digital signature is used to verify the sender's identity and ensure the data's integrity.

Web3: Everyday Transactions

In Web3, digital signatures are everywhere, they are used for signing transactions, messages, and verifying wallet ownership. For instance, Collab.Land uses your wallet's digital signature to verify your wallet address, ensuring secure connections.

How Digital Signatures Work

Let's look at a simple flow of how digital signatures work between two parties, Alice and Bob:

  1. Alice wants to send data to Bob.
  2. Alice possesses a private key (known only to her) and a public key (visible to anyone).
  3. She signs her message with her private key and sends both the message and the signature to Bob.
  4. Bob, to verify that the message came from Alice, uses her public key to check the signature against the received message.

Digital signatures are particularly crucial when sharing data over the public internet, which can be a daunting place. If you run an exposed API server on the internet, implementing security measures becomes essential. This is where Collab.Land's API and signature verification come into play.

Defending Against DDoS Attacks

One common threat on the internet is Distributed Denial of Service (DDoS) attacks. In these attacks, malicious actors flood a network or server with payloads until it becomes unresponsive. To mitigate these attacks, consider:

  • Rate-limiting API calls
  • Whitelisting or blacklisting IPs
  • Implementing Digital Signature Verification Algorithms
  • Using a Web Application Firewall

Implementing Signature Verification in Miniapps

Now, let's dive into how you can implement signature verification for your Collab.Land Miniapps:

  1. Always check for signature headers in your /interactions endpoint before processing any data.
  2. Verify the signature against Collab.Land's API public key and the received raw data. You can find Collab.Land API public keys at https://api.collab.land/config.
  3. Only trust and process the data when the signature is successfully verified. This ensures that the data was sent by Collab.Land and can be trusted.

Hands-On with Code

To experiment with implementing signature verification, you can use Collab.Land's Miniapp templates. Here's a step-by-step guide:

  1. Clone one of the Collab.Land Miniapp templates.
  2. Review the Editing Response Messages documentation to understand the expected request format.
  3. Open the .env file and set SKIP_VERIFICATION to true.
  4. Build and run the server with npm run build && npm run start.
  5. Follow the documentation to test your Miniapp in a Discord server.
  6. Send a malicious request to the server URL (ACTION URL). You'll notice that the server breaks.

Legitimate requests will fail as well, illustrating the importance of proper verification.

Protecting Your Miniapp

Collab.Land places a strong emphasis on security for miniapp developers. Here's how we ensure security:

  • Collab.Land's API always sends a digital signature in the request headers to the action URLs used by partner Miniapps.
  • We expose public keys that developers can use to verify webhook payloads.
  • We strongly recommend implementing signature verification in production to safeguard against attacks and protect user data.

Updating Your Codebase for Signature Verification

To implement signature verification in your codebase:

  1. Remove the SKIP_VERIFICATION variable or set it to false in the .env file.
  2. Restart the server.
  3. Resend a malicious payload. The API will respond with a denial because the signature verification headers are missing, ensuring that only requests from Collab.Land is accepted and processed.

Implementing Signature Verification in the Code

Collab.Land simplifies signature verification with an easy-to-use verify function. This function pulls in public keys from the /config endpoint for different environments (QA and Production). When a request hits the /interactions endpoint, the verify function runs first, checking verification headers, matching keys, and expiration.

Collab.Land supports two verification algorithms, ECDSA and ED25519, depending on the signature type. Signature verification is already baked into the Collab.Land Miniapp templates and all you need to do to enable it is set the SKIP_VERIFICATION env variable to true. However if you'd like to take a look at our implementation or roll your own, here's a sample code snippet:

Initialize the SignatureVerifier:

  static async initVerifier() {
const apiUrl = `https://api${
process.env.NODE_ENV === "production" ? "" : "-qa"
}.collab.land/config`;
const keysResponse = await fetch(apiUrl);
const keys = await handleFetchResponse<CollabLandConfig>(
keysResponse,
200,
{
customErrorMessage: `Error in fetching collab.land config from URL: ${apiUrl}`,
}
);
SignatureVerifier.ECDSAPublicKey = keys.actionEcdsaPublicKey;
SignatureVerifier.ED25519PublicKey = Buffer.from(
decode(keys.actionEd25519PublicKey)
).toString("hex");
debug("API URL for Collab.Land Config:", apiUrl);
debug("SingatureVerifier Initialized");
}

In the snippet above, we fetch the public keys from the /config endpoint and store them in the SignatureVerifier class. We also decode the ED25519 public key from base64 to hex.

Next, verify the signature:

  verify(req: Request, res: Response) {
if (!process.env.SKIP_VERIFICATION) {
try {
debug("Verifying signature...");
const ecdsaSignature = req.header(ActionEcdsaSignatureHeader);
const ed25519Signature = req.header(ActionEd25519SignatureHeader);
const signatureTimestamp: number = parseInt(
req.header(ActionSignatureTimestampHeader) ?? "0"
);
const body = JSON.stringify(req.body);
const signature = ecdsaSignature ?? ed25519Signature;
if (!signature) {
throw new HttpErrors[401](
`${ActionEcdsaSignatureHeader} or ${ActionEd25519SignatureHeader} header is required`
);
}
const signatureType =
signature === ecdsaSignature ? "ecdsa" : "ed25519";
const publicKey = this.getPublicKey(signatureType);
if (!publicKey) {
throw new HttpErrors[401](`Public key is not set.`);
}
this.verifyRequest(
body,
signatureTimestamp,
signature,
publicKey,
signatureType
);
return true;
} catch (err) {
if (HttpErrors.isHttpError(err)) {
res.status(err.statusCode).json({
message: err.message,
});
return false;
} else {
res.status(403).json({
message: "Unauthorized",
});
return false;
}
}
} else {
return true;
}
}

In the snippet above, we check if the SKIP_VERIFICATION env variable is set to true. If it is, we skip the verification process and return true. Otherwise, we check for the signature headers and verify the signature against the public key. If the signature is valid, we return true, otherwise we return false and send an error response.

Next, we define a function to get the Collab.Land public keys:

 private getPublicKey(signatureType: "ecdsa" | "ed25519") {
return signatureType === "ecdsa"
? SignatureVerifier.ECDSAPublicKey
: SignatureVerifier.ED25519PublicKey;
}

In the snippet above, we return the appropriate public key based on the signature type.

Next, we define a function to verify the signature using the ED25519 algorithm:

private verifyRequestWithEd25519(
publicKey: string,
signature: string,
body: string
) {
let verified = false;
try {
debug("Verifying webhook request with Ed25519 signature...");
debug(
"Public key: %s, signature: %s, message: %s",
publicKey,
signature,
body
);
verified =
signature != null &&
nacl.sign.detached.verify(
Buffer.from(body, "utf-8"),
Buffer.from(signature, "hex"),
Buffer.from(publicKey, "hex")
);
debug("Signature verified: %s", verified);
} catch (err: AnyType) {
verified = false;
debug(err.message);
}

if (!verified) {
throw new HttpErrors[403](
"Invalid request - Ed25519 signature cannot be verified."
);
}
return verified;
}

In the snippet above, we verify the signature using the ED25519 algorithm. We use the nacl library to verify the signature.

Consequently, we define a function to verify the signature using the ECDSA algorithm:

 private verifyRequestWithEcdsa(
publicKey: string,
signature: string,
body: string
) {
let verified = false;
try {
debug("Verifying webhook request with Ecdsa signature...");
debug(
"Public key: %s, signature: %s, message: %s",
publicKey,
signature,
body
);
const digest = utils.hashMessage(body);
verified =
signature != null &&
utils.recoverPublicKey(digest, signature) === publicKey;
debug("Signature verified: %s", verified);
} catch (err) {
debug("Fail to verify signature: %O", err);
verified = false;
}

if (!verified) {
debug("Invalid signature: %s, body: %s", signature, body);
throw new HttpErrors[403](
"Invalid request - Ecdsa signature cannot be verified."
);
}
return verified;
}

Here, we verify the signature using the ECDSA algorithm. We use the ethers library to verify the signature.

Finally, we define a function to verify the request payload:

private verifyRequest(
body: string,
signatureTimestamp: number,
signature: string,
publicKey: string,
signatureType = "ecdsa"
) {
const delta = Math.abs(Date.now() - signatureTimestamp);
if (delta >= 5 * 60 * 1000) {
throw new HttpErrors[403](
"Invalid request - signature timestamp is expired."
);
}
const msg = signatureTimestamp + body;
if (signatureType === "ed25519") {
this.verifyRequestWithEd25519(publicKey, signature, msg);
} else if (signatureType === "ecdsa") {
this.verifyRequestWithEcdsa(publicKey, signature, msg);
}
return JSON.parse(body);
}

In conclusion, implementing signature verification in your Collab.Land Miniapps is crucial for ensuring data security and protecting against potential threats. By following these steps and best practices, you can create a safer and more trustworthy environment for your users while building innovative Miniapps on the Collab.Land Marketplace. Happy coding and stay secure!

· 4 min read
Ekene Eze

Token gating (AKA Token Granted Access) is a concept that has gained significant traction in recent years, particularly within decentralized autonomous organizations (DAOs) and non-fungible token (NFT) communities. At its core, token gating involves using on-chain assets to grant or restrict access to a community or a resource.

The concept of token gating was first popularized by the Collab.Land team, who made it possible for DAOs and other tokenized communities to verify membership through the possession of on-chain assets. A good example of this is how communities like Axie Infinity use Collab.Land to authenticate new users who are joining their community. New members are required to go through a one-time wallet connection flow, after which they will be assigned roles based on their asset holdings. These roles will give them a tailored experience in the community, like access to exclusive messaging permissions, private channels etc.

The success of this approach grew the Collab.Land ecosystem to over 50k communities with a reach of over 100MM. That is huge, but we’re not stopping there! We are moving to the next stage - exposing our infrastructure to developers. What if developers had the ability to gate or grant access to their websites or specific parts of their projects based on the possession of some specified on-chain assets? Well, they can, with our website token gating APIs.

Introducing Collab.Land’s Token Gating APIs

In January, we announced a new /access-control endpoint for token gating that allows developers to control access to their applications based on the possession of specific on-chain tokens. The endpoint accepts two parameters:

  • account - The wallet address of the user.
  • rules - The token gating rules that specify the assets that qualify a user.

For each rule, Collab.Land will return a response indicating whether or not the rule has been granted or denied for the user. Here’s a sample request to the Collab.Land /access-control endpoint:

POST https://api.collab.land/access-control/check-roles
BODY:
{
"account": "0x01c20350ad8..............7bca295",
"rules": [
{
"type": "ERC721",
"chainId": 1,
"minToken": "1",
"contractAddress": "0xbc4ca0edaxxx8a936f13d",
"roleId": "001"
}
]
}

Where: account represents the user's wallet address and rules is an array of objects representing each rule to be checked against the user's wallet. This call would return a sample response like this:

{
"roles": [
{
"id": "001",
"granted": true
}
]
}

With this, developers can build simple and if needed, complex token gating configurations that allow them to offer custom experiences to qualified users. To learn more about the Collab.Land token gating APIs, please read the official documentation or dive into a practical tutorial to build a gated website in minutes. You can also get your hands dirty with the Github repo.

How to Get Started

First, head over to the Collab.Land DevPortal and create a client application. The client app will provide you with the necessary credentials (API Key, secret and client ID) needed to interact with the Collab.Land APIs. With the client app created, you can leverage our token gating tutorial or this video to gain a better understanding of our implementation detail. For instance, if you want to build a token gated website with JavaScript, here’s a sample code snippet to call the Collab.Land /check-roles endpoint using fetch:

const res = await fetch(`https://api.collab.land/access-control/check-roles`, {
method: 'POST',
headers: new Headers({
Accept: 'application/json',
'X-API-KEY': process.env.COLLABLAND_KEY,
'Content-Type': 'application/json',
}),
body: JSON.stringify({
account: myWalletAddr, // pass in the user's wallet address
rules: [{}, {}], // pass in the rules to be checked
}),
});

With our token gating APIs, we are extending the successful access control patterns in our communities and DAOs to developers. With one endpoint, developers can gate and grant access to websites, web applications, games, mobile applications and so on.

I look forward to seeing what you build, and when you do, I’d be happy to amplify it for you. I've also linked a video tutorial on the Getting Started section to show you how to implement this API in a Next.js project specifically.

· 5 min read
Ekene Eze

Read on to learn how TheSuccessFinder team leveraged Collab.Land's APIs to enhance the security and exclusivity of their online communities. This process is even easier now thanks to the launch of the Collab.Land Developer Portal.

Introducing TheSuccessFinder

TheSuccessFinder (TSF) is a community platform designed to foster effective communication, collaboration, and knowledge sharing. With its four main features—leaders, communities, knowledge center, and events—TSF aims to create safe and secure online communities that provide valuable experiences for its users.

TSF empowers leaders to create text and audio communication channels such as announcements, community conversations, courses, and audio rooms with just one click.

The community feature within TSF is a space for synchronous conversations and working together on various projects by posting, replying, and using video collaboration.

The knowledge center within TSF hosts blogs and vlogs. New members can search for leaders based on their interests and connect with them through these knowledge-sharing resources, and also allows them to access educational content and gain insights from leaders in their areas of interest.

Additionally, through events in TSF, leaders offer events to their community. Members can conveniently purchase them with simple payment processing.

TheSuccessFinder commits to creating a safe and secure environment for leaders, while also empowering learners to connect and collaborate effectively.

Early Challenges: Building a Rewarding Community Platform

When developing TSF, the team faced the challenge of finding an existing platform that would facilitate effective collaboration, communication, and knowledge sharing for its users. And so, TSF created a platform that offers the positive aspects of existing platforms while addressing their drawbacks to provide a custom experience for their users.

To start, TSF implemented a Know Your Customer (KYC) process for community leaders. KYC ensured that leaders are competent in the areas they wanted to create communities for, preventing unknowns from diluting the value and authenticity of the platform. TSF used custom authentication solutions to ensure that only authorized users gained access to the appropriate communities, improving member safety. The TSF team sought user feedback to identify and address any loopholes or vulnerabilities encountered during the implementation process.

During the development process, TSF became increasingly aware of web3 technologies and their security benefits. By studying platforms like Discord, Telegram, and Facebook groups, the team realized the potential of integrating web2 and web3 technologies to enhance security and authorization.

Solutions: Leveraging Collab.Land for Enhanced Security and Access Control

During their search for a token gating solution, TSF discovered Collab.Land. Collab.Land appeared to be the perfect fit for their needs. Then, TSF conducted extensive research and reached out to partners familiar with web3 technologies. All signs still pointed to Collab.Land, as they were already working with tens of thousands of communities on Discord and Telegram.

Collab.Land's strong reputation and long track record gave TSF the confidence to pursue an integration. By integrating Collab.Land, TSF gained the ability to token gate and allow community leaders to define access requirements for their members.

“One of the major advantages of integrating with Collab.Land was eliminating workarounds. With token gating, users either have the required token or they don't, which ensures a secure and streamlined access restriction process.” ~ Brandon Straza (Founder, TheSuccessFinder)

Collab.Land provides a complete suite of token gating functionalities that go beyond regular RPC calls to check the balance of a wallet. With their APIs, developers can leverage Collab.Land’s comprehensive and scalable token gating engines to extend their community and project’s gating functionalities beyond the scopes of Discord and Telegram.

The integration was initially intended to only gate access to the communities section, but TSF soon expanded the Collab.Land implementation to other areas of the platform, including the knowledge center. Using Collab.Land, blogs, courses, and video rooms can be token gated, enabling community leaders to easily monetize their content and resources. For example, TSF recently integrated token gating into secure video rooms, allowing leaders to ensure that only users with the necessary tokens can enter video meeting rooms for enhanced security and exclusivity.

Integrating with Collab.Land's Token Gating API

TSF's integration with Collab.Land established a double layer of security for both leaders and learners. The platform has significantly reduced the risk of both fake communities and the dissemination of false information. Now, individuals entering the community are verified before accessing content or engaging, providing tremendous value and confidence for both TSF leaders and learners.

The integration process with Collab.Land presented a steep learning curve for TSF's developers, as they had no prior experience with web3 technologies or token gating systems. However, the intuitive nature of Collab.Land's token gating APIs, coupled with the comprehensive documentation and responsive support from Collab.Land's DevRel team, facilitated a smooth integration process. Whenever clarification or assistance was needed, TSF's developers relied on Collab.Land's DevRel team, who promptly addressed their queries and provided guidance.

Collab.Land also provided a suite of developer tools to facilitate integration and development, some of which were the API reference, documentation, tutorials, guides, and a comprehensive SDK for frontend development.

This process has been simplified by the launch of Collab.Land's Developer Portal where developers self-serve and create their own client-apps, submit miniapps to the marketplace, and leverage the Login with Collab.Land (LWC) flow to activate members.

Looking to the future, TSF believes that token gating technology will continue to play a pivotal role in shaping online experiences. The platform has already adopted token gating in more areas than initially planned. TSF remains committed to an ongoing partnership with Collab.Land and the continued utilization of token gating technology.

Visit the Collab.Land Developer Portal to start building your own integration today!

· 4 min read
Ekene Eze

Collab.Land is proud to announce the launch of its new Developer Portal and API product offering. These pivotal additions serve to broaden accessibility for developers, paving the way for further innovation within the comprehensive Collab.Land network of communities.

Introducing the Developer Portal

The Developer Portal provides a dedicated platform for developers to contribute their Miniapps to:

  • Create client applications and get access to API credentials
  • Contribute Miniapps to the Collab.Land Marketplace.
  • Leverage LWC flow to request user permissions for data access.

This significant evolution enables developers to reach over 50,000 communities who are actively seeking to enhance their members' experiences. Through the dev portal, developers can build and submit miniapps to provide to a vast array of applications and services for community admins to customize their communities.

First Look at Collab.Land APIs

For the first time, the Developer Portal offers developers access to Collab.Land's APIs. These APIs encourage direct interaction with Collab.Land's network, facilitating a seamless integration process for active members across various websites, platforms, or apps.

Tiered API Product Offering

Designed with diverse needs in mind, Collab.Land's API product is available in three tiers.

Free Tier: Ideal for newcomers to the API, offering limited requests and rate limits for initial experimentation and familiarization as well as development.

  • 10 requests per second
  • 10000 requests per month
  • 5 Max marketplace keys
  • 5 Max client apps

Paid Tier: Geared towards developers requiring higher request and rate limits. This tier covers most use cases, perfectly suited for those ready to incorporate the API into larger projects:

  • 15 requests per second
  • 1000000 requests per month
  • 500 Max marketplace keys
  • 500 Max client apps

Enterprise Tier: Customized to suit specific requirements, the Enterprise Plan is for brands or projects that need custom support or integrations demanding additional resources from the core team. For those interested in this plan, please direct all inquiries to the Collab.Land team.

API Use Cases

Build token gated websites and web applications

Build token gated web apps to provide additional functionalities to your community, fans or customers. With the website token gating features, you can gate / grant access to exclusive content and features on your site. Learn more.

Reach 50k+ communities on Collab.Land with miniapps

Build lightweight implementations of your projects that run on the Collab.Land bot as a miniapp. They are a great way to get your project in front of 50k+ community admins and 9M+ users. Learn more

Request user permissions for data access with LWC Flow

Leverage the Collab.Land LWC flow to request user permissions to access their data on Collab.Land. This will allow you to access user wallets and build custom experiences in your projects. The LWC flow follows the OAuth2 protocol, allowing applications to obtain an access token that can be used to interact with the Collab.Land API on behalf of the authenticated user.

Collab.Land remains committed to privacy and security. Accordingly, any use of the community APIs that involves member data will necessitate explicit user consent, reflecting best practices in data privacy and giving members complete control over their personal information.

Empowering the Web3 Journey

At Collab.Land we are dedicated to empowering multiple personas on their journey through web3. By offering developers more extensive access and powerful tools, we are fostering innovation and promoting accessibility within our network. Simultaneously, we stay true to our commitment to safety and privacy. The launch of the Developer Portal and new API product signifies a significant milestone in our ongoing mission to create a more interactive, personalized, and secure digital ecosystem and we can't wait to see what you all build.