Upload Lens metadata

Storing Lens metadata on Bundlr

Lens Protocol is a composable and decentralized social graph; it allows you to quickly create social applications without having to build your own large backend services. The Lens documentation is extensive and very well done. The point of this guide isn't to replicate anything on the Lens site, rather it's a focused guide showing how to create and store your post metadata on Arweave via Bundlr. For additional information of creating a post on Lens, definitely spend some time on their docs. (opens in a new tab).

When working with Lens, you upload structured metadata to a permanent storage provider (Bundlr) and then pass the URL to that content to Lens.

There are two ways to interact with Lens:

  1. Using React hooks (opens in a new tab)
  2. Using calls to post() and postWithSig() (opens in a new tab)

React hooks

The React hooks for Lens make client-side development easier as streamline much of the development, including generating correct metadata for you to upload.

upload() pattern

When working with hooks like useUpdateProfileDetails() and useCreatePost(), Lens works using a pattern where you define a function called upload() that matches the following signature.

export const upload = (data: unknown): Promise<string> => {
    const serialized = JSON.stringify(data);
    const url = // upload serialized to a public location
    return url;

The upload() function is passed to the hooks on initialization, the hooks then prepare the required metadata and pass it to upload() in the data parameter. In implementing the function, it's up to you to serialize the metadata (JSON object), store it on Bundlr and then return the URL from the function. The hook then takes the URL to the uploaded metadata and continues using it to update profile details or create a new post.

A full ready-to-use implementation of the upload() function is as follows:

import { WebBundlr } from "@bundlr-network/client";
import { getBundlr } from "./get-bundlr";
import { fetchSigner } from "wagmi/actions";
const getBundlr = async () => {
	const signer = await fetchSigner();
	const provider = signer?.provider;
	const bundlr = new WebBundlr("https://node2.bundlr.network", "matic", provider);
	await bundlr.ready();
	return bundlr;
export const upload = async (data) => {
	try {
		const bundlr = await getBundlr();
		const serialized = JSON.stringify(data);
		// fund (if needed)
		const price = await bundlr.getPrice(new Blob([serialized]).size);
		await bundlr.fund(price);
		const tx = await bundlr.upload(serialized, {
			tags: [{ name: "Content-Type", value: "application/json" }],
		console.log(`Upload success content URL= https://arweave.net/${tx.id}`);
		return `https://arweave.net/${tx.id}`;
	} catch (e) {
		console.log("error on upload ", e);
	return "";

If you incorrectly set the Content-Type attribute, Lens will not read your data.

Using post() and postWithSig()

When working with the functions post() and postWithSig(), it is your responsibility to correctly build the required metadata for each interaction, store it on Bundlr and then pass the URL to Lens.

Uploading metadata

The full Lens Protocol metadata specification (opens in a new tab) details how to create just about any type of post. For this guide, we're focusing on an image post and will use the following metadata.

	"version": "2.0.0",
	"metadata_id": "${uuid()}",
	"description": "gm (🌿, 🌿)",
	"image": "https://arweave.net/CO9EpX0lekJEfXUOeXncUmMuG8eEp5WJHXl9U9yZUYA",
	"imageMimeType": "image/png",
	"name": "Post by foo.lens",
	"attributes": [{ "traitType": "type", "value": "POST" }],
	"media": [
			"item": "https://arweave.net/CO9EpX0lekJEfXUOeXncUmMuG8eEp5WJHXl9U9yZUYA",
			"type": "image/png",
			"altTag": ""
	"appId": "MyApp",
	"locale": "en",
	"mainContentFocus": "IMAGE"

The function uuid() generates a unique post id using the UUID npm package (opens in a new tab).

To upload to Bundlr, first stringify the data, then pass it to bundlr.upload(), tagging it with the Content-Type of application/json.

// Create a WebBundlr object
const bundlr = new WebBundlr("https://node2.bundlr.network", "matic", provider);
await bundlr.ready();
// Stringify our data
const data = JSON.stringify(metadata);
// Upload to Arweave via Bundlr
const tx = await bundlr.upload(data, {
	tags: [{ name: "Content-Type", value: "application/json" }],
// Print our URI, note use of HTTPS
console.log(`Upload success content URI= https://arweave.net/${tx.id}`);

If you incorrectly set the Content-Type attribute, Lens will not read your data.

Creating a post

After successfully uploading your metadata, you'll get a transaction id that can be combined with https://arweave.net/ to create your content URI (https://arweave.net/6eARMSxeRRhOU0iJEivNB_naY-YFuh_D9d0-58ccKfI).

Finally, create a post request using the generated content URL and pass to the Lens contract calling either of the .post() or .postWithSig() functions.

const tx = await lensHub.postWithSig({
	profileId: typedData.value.profileId,
	contentURI: typedData.value.contentURI, // URL from the metadata uploaded above
	collectModule: typedData.value.collectModule,
	collectModuleInitData: typedData.value.collectModuleInitData,
	referenceModule: typedData.value.referenceModule,
	referenceModuleInitData: typedData.value.referenceModuleInitData,
	sig: {
		deadline: typedData.value.deadline,

While http://arweave.net/[transaction-id] and https://arweave.net/[transaction-id] will both work in your browser, only the https version will work when creating your Lens post.