Guides

Onboarding

Learn how to onboard new users to Lens.

The process of onboarding a new user involves minting a new Profile NFT. This can be accomplished either by the user themselves in a self-funded manner, or by a third party on behalf of the user.

Choosing a Handle

Typically, users will choose their Handle during the onboarding process. They can only choose the local-name portion of the Handle, such as wagmi in lens/wagmi. The namespace is currently fixed to lens.

Here are the rules to follow when choosing an Handle:

  • Length: The local-name should be between 5-26 characters. For example, lens/wagmi is correct, but lens/wagm is too short.
  • Uniqueness: The Handle must be unique. If you try to create a Handle that already exists, the minting will fail.
  • Character Types: Handles must consist of alphanumeric characters and underscores (_), but they must not start with _ or a digit (0-9).
  • Case Insensitivity: Handles are case-insensitive. So lens/wagmi and lens/Wagmi are considered the same Handle.

Crypto Onboarding

In crypto self-funded onboarding, the user is guided through the process of creating a Lens Profile with their desired Handle for a fee of 8 MATIC. As this process uses the user's wallet to send the transaction, it cannot be gasless and requires the user's signature.

📘

The fee may vary based on the market value of MATIC, with the aim of maintaining a consistent onboarding cost.

React SDK

The examples provided use @lens-protocol/react-web for creating an onboarding webpage. This process can also be applied in React Native using the @lens-protocol/react-native package.

Start by connecting the user's wallet. Although the example uses a basic Wagmi setup, it can be adapted to suit different requirements.

import { useAccount, useConnect } from "wagmi";
import { injected } from "wagmi/connectors";

import { CreateProfileForm } from "./CreateProfileForm";

export function OnboardingPage() {
  const { address, isDisconnected, isConnecting } = useAccount();
  const { connect } = useConnect();

  if (isDisconnected && !address) {
    return (
      <button
        disabled={isConnecting}
        onClick={() => connect({ connector: injected() })}
      >
        Connect Wallet
      </button>
    );
  }

  return <CreateProfileForm wallet={address} />;
}

Next, we'll focus on the <CreateProfileForm> component, which allows the user to choose their desired Handle.

📘

Local-name only

Keep in mind that users can only choose the local-name portion of the Handle. The namespace is reserved for future use and it's currently just lens.

You can use the useValidateHandle hook to check if the desired handle is available.

import { useState } from "react";
import { useValidateHandle } from "@lens-protocol/react-web";

type CreateProfileFormProps = {
  wallet: string;
};

export function CreateProfileForm({ wallet }: CreateProfileFormProps) {
  const [localName, setLocalName] = useState("");
  const { execute: validateHandle, loading: verifying } = useValidateHandle();

  const submit = async () => {
    const result = await validateHandle({ localName });

    if (result.isFailure()) {
      console.error();
      return;
    }

    // TODO: mint profile
  };

  return (
    <form onSubmit={submit}>
      <input
        type="text"
        disabled={verifying}
        value={localName}
        onChange={(e) => setLocalName(e.target.value)}
      />

      <button type="submit" disabled={verifying}>
        Create
      </button>
    </form>
  );
}

Finally, use the useCreateProfile hook to mint the new Profile.

import { useState } from "react";
import { useCreateProfile, useValidateHandle } from "@lens-protocol/react-web";

type CreateProfileFormProps = {
  wallet: string;
};

export function CreateProfileForm({ wallet }: CreateProfileFormProps) {
  const [localName, setLocalName] = useState("");
  const { execute: validateHandle, loading: verifying } = useValidateHandle();
  const { execute: createProfile, loading: creating } = useCreateProfile();

  const submit = async () => {
    const result = await validateHandle({ localName });

    if (result.isFailure()) {
      window.alert(result.error.message);
      return;
    }

    const result = await execute({ localName, to: address });

    if (result.isFailure()) {
      window.alert(result.error.message);
      return;
    }

    const profile = result.value;
    window.alert(
      `Congratulations! You now own: ${profile.handle?.fullHandle}!`
    );
  };

  return (
    <form onSubmit={submit}>
      <input
        type="text"
        disabled={verifying || creating}
        value={localName}
        onChange={(e) => setLocalName(e.target.value)}
      />

      <button type="submit" disabled={verifying || creating}>
        Create
      </button>
    </form>
  );
}

That's it–you can now log in the user with the useLogin hook and their newly created Profile ID.

Manual Contract Call

You can onboard a new user to Lens by allowing them to mint a profile with a handle using the new PermissionlessCreator smart contract directly. This section will guide you through the process of minting a Lens profile with a handle using the JavaScript SDK (i.e. LensClient) or the Lens GraphQL API.

The process is as follows:

  1. Validate the requested new handle and check its availability.
  2. Gather the necessary input data to mint a new profile with a handle.
  3. Submit the mint transaction.
  4. Proceed with optional next steps.

1. Validate the Requested Handle

The handle must adhere to specific rules to be considered valid. The Lens SDKs provide a straightforward isValidHandle function to verify the validity of a handle.

import { isValidHandle } from "@lens-protocol/client";

const requestedHandle = "wagmi"; // input from the user

if (!isValidHandle(requestedHandle)) {
  console.error(`Invalid handle`);
}

The next step is to verify if the requested handle is available. Here's how you can do it using the LensClient.

import { LensClient, production } from "@lens-protocol/client";

const client = new LensClient({
  environment: production,
});

const handleOwnerAddress = await client.handle.resolveAddress({
  handle: `lens/${requestedHandle}`,
});

if (handleOwnerAddress) {
  console.error(`The requested handle is not available.`);
}

You can also directly query the Lens GraphQL API.

query {  
  handleToAddress(request: { handle: "lens/wagmi" })  
}
{
  "data": {
    "handleToAddress": null // or owner wallet address
  }
}

2. Gather the Required Data for Minting

The PermissionlessCreator smart contract provides two methods that are crucial for this tutorial. Below is a snippet from the ABI. You can find the addresses of the deployed contracts page.

[
  {
    "inputs": [],
    "name": "getProfileWithHandleCreationPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "to",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "followModule",
            "type": "address"
          },
          {
            "internalType": "bytes",
            "name": "followModuleInitData",
            "type": "bytes"
          }
        ],
        "internalType": "struct Types.CreateProfileParams",
        "name": "createProfileParams",
        "type": "tuple"
      },
      {
        "internalType": "string",
        "name": "handle",
        "type": "string"
      },
      {
        "internalType": "address[]",
        "name": "delegatedExecutors",
        "type": "address[]"
      }
    ],
    "name": "createProfileWithHandle",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "profileId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "handleId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  }
]

The createProfileWithHandle method is what we'll use to mint a new profile with a handle. To call this method, we need to provide the following parameters:

  • to: The wallet address of the new profile owner.
  • handle: The local-name part of a requested handle. The namespace is assigned automaticaly.
  • delegatedExecutors: An array of addresses that are allowed to execute transactions on behalf of the new profile owner. This enables the Profile Manager to be activated along with the profile creation.
  • value: The price to mint a new profile with a handle.
  • followModule and followModuleInitData: These are for advanced use and we'll leave them out for now.

To enable the Signless Experience you will need to provide the Lens Relayer address as one of the Profile Managers. You can retrieve this address from the Lens API using the JavaScript SDK (i.e. LensClient) or the Lens GraphQL API.

const relayerAddress = await client.transaction.generateLensAPIRelayAddress();
query {
  generateLensAPIRelayAddress
}

You can retrieve the price of minting a new profile with a handle using the getProfileWithHandleCreationPrice method.

import { createPublicClient, createWalletClient, http, custom } from 'viem'
import { mainnet } from 'viem/chains'

import abi from "./permissonless-creator.json" assert { type: "json" };

// setup all what is required to interact with the contract
export const publicClient = createPublicClient({
  chain: mainnet,
  transport: http(),
})
export const walletClient = createWalletClient({
  chain: mainnet,
  transport: custom(window.ethereum!),
})

// init the contract
const permissonlessCreator = getContract({
  address: CONTRACT_ADDRESS,
  abi,
  client: { public: publicClient, wallet: walletClient }
});

// get the mint price
const price = await permissonlessCreator.read.getProfileWithHandleCreationPrice();
import { ethers } from "ethers";
import abi from "./permissonless-creator.json" assert { type: "json" };

// setup all what is required to interact with the contract
const provider = new ethers.providers.JsonRpcProvider(RPC_PROVIDER_URL);
const wallet = new ethers.Wallet(WALLET_PRIVATE_KEY, provider);

// init the contract
const permissonlessCreator = new ethers.Contract(
  CONTRACT_ADDRESS,
  abi,
  wallet
) as PermissonlessCreator;

// get the mint price
const price = await permissonlessCreator.getProfileWithHandleCreationPrice();

3. Submit the Mint Transaction

Now that we have all the required data, we can proceed to mint a new profile with a handle. We also have an instance of the PermissionlessCreator smart contract ready to use from the previous step.

Let's go ahead and submit the mint transaction.

const hash = await permissonlessCreator.write.createProfileWithHandle(
  {
    to: wallet.address,
    followModule: "0x0000000000000000000000000000000000000000",
    followModuleInitData: "0x",
  },
  requestedHandle, // "wagmi"
  [relayerAddress],
  {
    value: price,
  }
);

console.log(
  `Transaction to create a new profile with handle was successfully broadcasted with hash`,
  hash
);

console.log(`Waiting for the transaction to be indexed...`);
const outcome = await client.transaction.waitUntilComplete({
  forTxHash: hash,
});

if (outcome === null) {
  console.error(`The transaction with hash ${hash} was lost.`);
  process.exit(1);
}

console.log("A new profile with handle has been successfully minted.");
const tx = await permissonlessCreator.createProfileWithHandle(
  {
    to: wallet.address,
    followModule: "0x0000000000000000000000000000000000000000",
    followModuleInitData: "0x",
  },
  requestedHandle, // "wagmi"
  [relayerAddress],
  {
    value: price,
  }
);

console.log(
  `Transaction to create a new profile with handle was successfully broadcasted with hash`,
  tx.hash
);

console.log(`Waiting for the transaction to be indexed...`);
const outcome = await client.transaction.waitUntilComplete({
  forTxHash: tx.hash,
});

if (outcome === null) {
  console.error(`The transaction with hash ${tx.hash} was lost.`);
  process.exit(1);
}

console.log("A new profile with handle has been successfully minted.");

That's it—you've successfully minted a new profile with a handle using the PermissionlessCreator smart contract.

4. Optional Next Steps

After the transaction is indexed by Lens API we recommend the following next steps:

  1. Fetch the newly minted profile.
  2. Authenticate the profile with the API.
  3. Update the Profile Metadata.

The steps you choose to implement depend on your application's needs.

The complete onboarding script using the JavaScript SDK can be found in the examples folder of the Lens SDK GitHub repository.

Credited Onboarding

For applications with unique onboarding requirements, a credit system for onboarding is available to cater to these needs.

Recognized app builders are granted an initial number of credits by the Lens Protocol team. These credits can be used to mint new Profile in behalf of the app new users. Each credit used reduces the total number of credits available to the app.

📘

If you choose to charge users for onboarding with this integration, you must align with the Lens Protocol fees, which are 8 MATIC for crypto payments or 10 USD for fiat payments.

If you're interested in obtaining credits for your app, please reach out to the Lens Protocol team via this form.

Below are the PermissionlessCreator contract features that can be utilized to mint new Profiles using credits.

Profile Minting

The createProfileWithHandleUsingCredits function, available on the PermissionlessCreator smart contract, allows the minting of a new Profile with Handle using credits. This function operates similarly to createProfileWithHandle, but it doesn't require a fee. Instead, the app builder can utilize their credits to mint the new profile for the user.

📘

Note

This function must be invoked from the address that holds the credits.

function createProfileWithHandleUsingCredits(
  Types.CreateProfileParams calldata createProfileParams,
  string calldata handle,
  address[] calldata delegatedExecutors
) external returns (uint256 profileId, uint256 handleId);

struct CreateProfileParams {
  address to;
  address followModule;
  bytes followModuleInitData;
}

The app builder can use the delegatedExecutors parameter to set up Profile Manager addresses on the newly created Profile. If the Lens Relayer address is included in the delegatedExecutors array, this effectively enables the Signless Experience for the new Profile.

Lazy Onboarding

We've made it possible to mint Profile and Handle independently using credits. Consider minting a batch of Profiles without Handles in advance. These can be held in reserve and transferred to new users during their onboarding process. The Handle can then be minted lazily, according to the user's request.

This method provides a seamless experience, enabling users to authenticate and start using the app immediately. The Handle is minted in the background, and once it's successfully created, it can be transferred and linked to the user's Profile smoothly.

Use the createProfileUsingCredits function to mint a new Profile without a Handle using credits.

function createProfileUsingCredits(
  Types.CreateProfileParams calldata createProfileParams,
  address[] calldata delegatedExecutors
) external returns (uint256);

struct CreateProfileParams {
  address to;
  address followModule;
  bytes followModuleInitData;
}

Use the createHandleWithCredits function to mint a new Handle using credits.

function createHandleWithCredits(
  address to,
  string calldata handle
) external returns (uint256);

Use the standard ERC-721 transferFrom method on the Handle NFT contract (refer to LensHandles in smart contracts) to transfer the Handle to the user.

Transfer Profile

Once the Profile is minted, the app builder can transfer it to the user using the standard ERC-721 transferFrom method on the Profile NFT contract (refer to LensHub in smart contracts).

If the Profile was created with delegatedExecutors (i.e., Profile Manager addresses), the transferFromKeepingDelegates function should be used to maintain this configuration after transferring the Profile to the user. This function is available on the PermissionlessCreator smart contract.

function transferFromKeepingDelegates(
  address from,
  address to,
  uint256 tokenId
) external;

📘

Note

This function must be invoked by the address that initially minted the profile.

Creative Approaches

It's worth noting that the credit address you provide to the Lens Protocol team can be a smart contract. This allows you to incorporate flexible rules into your onboarding process. For instance, you could set conditions such as requiring users to hold a specific NFT, or you could charge your own fee for onboarding. All these conditions can be composed on-chain.

📘

Note

We recommend making your contracts upgradable proxies to easily adapt their criteria as needs evolve.

Below, we provide two examples to illustrate the simplicity of creating a wrapper contract. However, the possibilities are endless, and you can customize your contract to suit your specific needs.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';

struct CreateProfileParams {
    address to;
    address followModule;
    bytes followModuleInitData;
}

interface IPermissonlessCreator {
    function createProfileUsingCredits(
        CreateProfileParams calldata createProfileParams,
        address[] calldata delegatedExecutors
    ) external returns (uint256);

    function createProfileWithHandleCredits(
        CreateProfileParams calldata createProfileParams,
        string calldata handle,
        address[] calldata delegatedExecutors
    ) external returns (uint256, uint256);

    function createHandleWithCredits(
        address to,
        string calldata handle
    ) external returns (uint256);
}

contract AppChargeLensOnboarding is Ownable {
    // load the permissonless creator contract
    IPermissonlessCreator public immutable PERMISSONLESS_CREATOR;

    // the cost to buy a profile with handle
    uint256 public profileWithHandleCreationCost = 1 ether;

    error InvalidFunds();

    constructor(address owner, address permissonlessCreator) {
        _transferOwnership(owner);
        PERMISSONLESS_CREATOR = IPermissonlessCreator(permissonlessCreator);
    }

    // charge funds send to your beneficiary and then create profile for the user
    function createProfileWithHandle(
        CreateProfileParams calldata createProfileParams,
        string calldata handle,
        address[] calldata delegatedExecutors
    ) external payable returns (uint256 profileId, uint256 handleId) {
        if (msg.value != profileWithHandleCreationCost) {
            revert InvalidFunds();
        }

        // delegatedExecutors are only allowed if to == msg.sender
        if (delegatedExecutors.length > 0 && createProfileParams.to != msg.sender) {
            revert NotAllowed();
        }

        return PERMISSONLESS_CREATOR.createProfileWithHandleCredits(createProfileParams, handle, delegatedExecutors);
    }

    function withdrawFunds() external onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }
}
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';

struct CreateProfileParams {
    address to;
    address followModule;
    bytes followModuleInitData;
}

interface IPermissonlessCreator {
    function createProfileUsingCredits(
        CreateProfileParams calldata createProfileParams,
        address[] calldata delegatedExecutors
    ) external returns (uint256);

    function createProfileWithHandleCredits(
        CreateProfileParams calldata createProfileParams,
        string calldata handle,
        address[] calldata delegatedExecutors
    ) external returns (uint256, uint256);

    function createHandleWithCredits(
        address to,
        string calldata handle
    ) external returns (uint256);
}

// onboard lens users for free
contract AppFreeLensOnboarding is Ownable {
    // load the permissionless creator contract
    IPermissonlessCreator public immutable PERMISSONLESS_CREATOR;

    // your EOAs which can do this transactions can be
    // a list of defender addresses or a list of your own EOAs
    // these will send the transactions
    mapping(address => bool) public allowedAddresses;

    // modifier to only allow your allowed address to do it
    modifier onlyAllowed() {
        require(allowedAddresses[msg.sender], 'AppFreeLensOnboarding: Not allowed');
        _;
    }

    constructor(address owner, address permissonlessCreator) {
        _transferOwnership(owner);
        PERMISSONLESS_CREATOR = IPermissonlessCreator(permissonlessCreator);
    }

    // add to the allowed addresses
    function addAllowedAddresses(address[] calldata newAddresses) external onlyOwner {
        for (uint256 i = 0; i < newAddresses.length; i++) {
            allowedAddresses[newAddresses[i]] = true;
        }
    }

    // remove the allowed addresses
    function removeAllowedAddress(address newAddress) external onlyOwner {
        allowedAddresses[newAddress] = false;
    }

    // check if they are allowed addresses
    function isAllowedAddress(address newAddress) external view returns (bool) {
        return allowedAddresses[newAddress];
    }

    // checks onlyAllowed and then mints it from the credits
    function createProfileWithHandle(
        CreateProfileParams calldata createProfileParams,
        string calldata handle,
        address[] calldata delegatedExecutors
    ) external onlyAllowed returns (uint256 profileId, uint256 handleId) {
        return PERMISSONLESS_CREATOR.createProfileWithHandleCredits(createProfileParams, handle, delegatedExecutors);

}

Alternatively, you could use an Externally Owned Account (EOA) and manage the gating on the backend. This flexibility makes it a highly adaptable onboarding system.

Card Onboarding

In the card onboarding, the user is guided through the process of creating a Lens Profile with their desired Handle for a fee of 10 USD.

This feature is enabled through an integration with Stripe, a popular payment gateway. The Custom payment flow from Stripe is used, offering flexibility to customize the payment experience to suit your needs.

📘

Please note that this guide is more complex and requires your application to have a server-side component to manage server-to-server requests.

Integration Overview

The following sequence diagrams shows in broad strokes how the integration works.

The process unfolds as follows:

  1. Upon user initiation, the UI creates a Payment Intent via a bespoke server-side endpoint and initializes the Stripe Checkout form using the obtained Client Secret.
  2. The user enters their card details and follows the payment instructions provided by their card provider.
  3. Once the payment is successful, Stripe communicates with the Lens API, triggering the on-chain Profile creation process. Stripe also redirects the UI to the specified return_url.
  4. The UI verifies the payment outcome using the Client Secret from step 1.
  5. After verifying the successful payment, the UI begins checking for Blockchain Transaction Info associated with the Payment Intent ID from step 1.
  6. Once the UI receives confirmation from the Lens API, it waits for the completion of the Create Profile Transaction. This process uses the Transaction ID obtained earlier, as explained in the Lens Transaction Status guide.

Finally, the UI can fetch the newly created Profile and continue with the onboarding process.

Shared Secret

The Lens API provides two bespoke endpoints for this integration:

  • POST /payments/create
  • GET /payments/<paymentIntentId>/blockchain-tx-info

These endpoints are available at the specified paths on the Lens API URL for each environment.

To use these endpoints, contact the Lens Protocol team to obtain a shared secret for your application. This shared secret should be included in the x-shared-secret HTTP headers when making requests to these endpoints.

📘

This shared secret is a sensitive piece of information intended for server-to-server communication. It should never be exposed on the client-side.

Create Payment Intent

Use the POST /payments/create endpoint to create a Payment Intent. This endpoint requires the user's wallet address and the desired Handle local-name. The Lens API will respond with the Payment Intent's client secret, which you can use to initiate the Stripe Checkout form.

Request

POST /payments/create
Host: <api-url>
x-shared-secret: <shared-secret>
Accept: application/json
Content-Type: application/json
Content-Length: 103

{
  "address": "0x1234567890abcdef1234567890abcdef12345678",
  "handle": "wagmi",
  "currency": "usd"
}
Body Parameter
addressThe user's wallet address. If the payment is successful, Profile and Handle NFTs will be minted to this address.
handleThe local-name portion of the desired Handle. For example, for lens/wagmi, the handle is wagmi.
currency (optional)The currency for the payment. Currently, only usd is supported. If not specified, it defaults to usd.

Response

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 137

{
  "id": "pi_1J3xjz2eZvKYlo2C5z3z",
  "for": "0x1234567890abcdef1234567890abcdef12345678",
 "clientSecret": "pi_1J3xjz2eZvKYlo2C5z3z"
}
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Content-Length: 21

HANDLE_ALREADY_EXISTS
Response PropertyDescription
idThe Payment ID. This will be used to retrieve the Transaction ID associated with the payment.
forThis is the ethereum address the profile and handle will be minted to if the payment succeeds
clientSecretThe Payment Intent's client secret to be used with Stripe SDK.

Failures

Status CodeBody
400INVALID_CURRENCY
400INVALID_ETHEREUM_ADDRESS
400INVALID_HANDLE
400HANDLE_ALREADY_EXISTS
400FAILED_TO_CREATE_PAYMENT

Get Blockchain Info

Use the GET /payments/<paymentIntentId>/blockchain-tx-info endpoint to retrieve the Blockchain Transaction Info associated with a specific Payment Intent ID that was previously created.

Request

GET /payments/<paymentIntentId>/blockchain-tx-info
Host: <api-url>
x-shared-secret: <shared-secret>
Accept: application/json
URL ParameterDescription
paymentIntentIdThe Payment Indent ID from the POST /payments/create response.

Response

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 160

{
  "status": "FULL_SUCCESS",
  "txId": "a8b3a0f9-87d7-42e5-b214-31ea3b76247b",
  "handle": "wagmi",
  "address": "0x1234567890abcdef1234567890abcdef12345678"
}
Response PropertyValueDescription
statusThe status of the Card Onboarding.
CREATED_PAYMENTThe Payment Intent has been created.
PROCESSING_PAYMENTThe payment is currently being processed.
FAILED_PAYMENTThe payment has failed.
SUCCESS_PAYMENTThe payment was successful, but the Create Profile transaction has not been sent yet.
FULL_SUCCESSThe payment was successful and the Create Profile transaction has been sent.
SUCESSS_PAYMENT_BLOCKCHAIN_FAILEDThe payment was successful, but the Create Profile transaction failed. This is a rare case that requires manual intervention. Contact the Lens Protocol team if this occurs.
txIdstringWhen the status is FULL_SUCCESS, this is the Transaction ID of the Create Profile transaction. It is null otherwise.
handlestringThe desired handle.
addressstringThe user's wallet address.

Examples

Next.js

Here are Next.js examples demonstrating how to perform server-to-server requests. These examples use the App Router, but they can be adapted to use the Pages Router.

export const dynamic = "force-dynamic";

export async function POST(request: Request) {
  const response = await fetch(`${process.env.LENS_API}/payments/create`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-shared-secret": process.env.SHARED_SECRET,
    },
    body: await request.text(),
  });

  if (response.ok) {
    const data = await response.json();
    return Response.json({ data, success: true }, { status: 200 });
  }

  const error = await response.text();
  return Response.json(
    { error, success: false },
    { status: response.status }
  );
}
export const dynamic = "force-dynamic";

type Params = { paymentIntentId: string };

export async function GET(request: Request, { params }: { params: Params }) {
  const response = await fetch(
    `${process.env.LENS_API}/payments/${params.paymentIntentId}/blockchain-tx-info`,
    {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
        "x-shared-secret": process.env.SHARED_SECRET,
      },
    }
  );

  if (response.ok) {
    const data = await response.json();
    return Response.json({ data, success: true }, { status: 200 });
  }

  const error = await response.text();
  return Response.json(
    { error, success: false },
    { status: response.status }
  );
}

Express.js

Here is an Express.js server example demonstrating how to perform server-to-server requests.

const express = require("express");
const bodyParser = require("body-parser");

const app = express();

app.use(bodyParser.json());

app.post("/api/payments", async (req, res) => {
  const response = await fetch(`${process.env.LENS_API}/payments/create`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-shared-secret": process.env.SHARED_SECRET,
    },
    body: await request.text(),
  });

  if (response.ok) {
    const data = await response.json();
    return res.json({ data, success: true });
  }

  const error = await response.text();
  return res.json({ error, success: false });
});

app.get("/api/payments/:paymentIntentId", async (req, res) => {
  const response = await fetch(
    `${process.env.LENS_API}/payments/${req.params.paymentIntentId}/blockchain-tx-info`,
    {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
        "x-shared-secret": process.env.SHARED_SECRET,
      },
    }
  );

  if (response.ok) {
    const data = await response.json();
    return res.json({ data, success: true });
  }

  const error = await response.text();
  return res.json({ error, success: false });
});

app.listen(process.env.PORT, function () {
  console.log(`Listening on port: ${process.env.PORT}`);
});

Test Card Details

For testing your integration against the Amoy Testnet deployment, you can utilize the following test card details.

Test Card
Card number4242 4242 4242 4242
Expiry Dateany future date, e.g. 01/42
CVCany 3 digits, e.g. 123
Postal codeany valid postal code, e.g. KT4 7DD for a UK postcode

Additional Options

Testnet Profiles

We understand that the onboarding process can be challenging when developing new applications and needing multiple test profiles. To simplify this, we've made it possible to programmatically create Testnet Profiles using a Testnet-only API feature.

JavaScript SDK

The client.wallet.createProfileWithHandle method enables you to create a Lens Profile with a given Handle.

import { LensClient, development, isRelaySuccess } from "@lens-protocol/client";

const client = new LensClient({
  environment: development, // wont't work with `production`
});

const result = await client.wallet.createProfileWithHandle({
  // e.g. 'alice' which will be '@lens/alice' in full-handle notation
  handle: "<local name>",
  to: "<your address>",
});

// handle relay errors
if (!isRelaySuccess(profileCreateResult)) {
  console.error(`Something went wrong`, result);
  process.exit(1);
}

// wait for the transaction to be mined and indexed
const outcome = await client.transaction.waitUntilComplete({
  forTxId: result.txId,
});

// handle transaction not found
if (outcome === null) {
  console.error("The transaction was not found");
  process.exit(1);
}

console.log("Profile created");

API

The createProfileWithHandle mutation enables you to create a Profile with a given Handle.

mutation {
  createProfileWithHandle(
    request: {
      # e.g. 'alice' which will be '@lens/alice' in full-handle notation
      handle: "<local name>"
      to: "<your address>"
    }
  ) {
    ... on RelaySuccess {
      txHash
      txId
    }

    ... on CreateProfileWithHandleErrorResult {
      reason
    }
  }
}
{
  "data": {
    "actOnOpenAction": {
      "txId": "c30869d4-9a3a-4373-b5dc-811ea393bdf5",
      "txHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
    }
  }
}
{
  "data": {
    "actOnOpenAction": {
      "reason": "FAILED" // LensProfileManagerRelayErrorReasonType
    }
  }
}

The RelaySuccess#txId can be used to wait for the transaction to be mined and indexed like explained here.