Skip to content

Build custom content types

Any developer building with XMTP can create a custom content type and immediately start using it in their app. Unlike a standard content type, use of a custom content type doesn't require prerequisite formal adoption through the XRC and XIP processes.

Building a custom content type enables you to manage data in a way that's more personalized or specialized to the needs of your app.

For example, if you need a content type that isn't covered by a standard or standards-track content type, you can create a custom content type and begin using it immediately in your app.

Fallback plain text is "alt text"-like description text that you can associate with a custom content type if you are concerned that a receiving app might not be able to handle the content. If the receiving app is unable to handle the custom content, it displays the fallback plain text instead.

If another app wants to display your custom content type, they must implement your custom content type in their code exactly as it's defined in your code.

For more common content types, you can usually find a standard or standards-track content type to serve your needs.

If your custom content type generates interest within the developer community, consider proposing it as a standard content type through the XIP process.

This document describes how to build custom content types using two examples:

  • Build a basic custom content type that multiplies numbers
  • Build an advanced custom content type that sends a transaction hash

Basic: Build a custom content type that multiplies numbers

Create the content type

Create the custom content type by creating a new file

Browser
// A test of this content type can be found in the following PR: https://github.com/xmtp/xmtp-js/pull/509/files
import { ContentTypeId } from "@xmtp/content-type-primitives";
import type { ContentCodec, EncodedContent } from "@xmtp/content-type-primitives";
 
// Create a unique identifier for your content type
export const ContentTypeMultiplyNumbers = new ContentTypeId({
  authorityId: 'your.domain',
  typeId: 'multiply-number',
  versionMajor: 1,
  versionMinor: 0,
})
 
// Define the MultiplyNumbers class
export class MultiplyNumbers {
  public num1: number
  public num2: number
  public result: number | undefined
 
  constructor(num1: number, num2: number, result?: number) {
    this.num1 = num1
    this.num2 = num2
    this.result = result
  }
}
 
// Define the MultiplyCodec class
export class ContentTypeMultiplyNumberCodec
  implements ContentCodec<MultiplyNumbers>
{
  get contentType(): ContentTypeId {
    return ContentTypeMultiplyNumbers
  }
 
  // The encode method accepts an object of MultiplyNumbers and encodes it as a byte array
  encode(numbers: MultiplyNumbers): EncodedContent {
    const { num1, num2 } = numbers
    return {
      type: ContentTypeMultiplyNumbers,
      parameters: {},
      content: new TextEncoder().encode(JSON.stringify({ num1, num2 })),
    }
  }
 
  // The decode method decodes the byte array, parses the string into numbers (num1, num2), and returns their product
  decode(encodedContent: EncodedContent): MultiplyNumbers {
    const decodedContent = new TextDecoder().decode(encodedContent.content)
    const { num1, num2 } = JSON.parse(decodedContent)
    return new MultiplyNumbers(num1, num2, num1 * num2)
  }
 
  fallback(content: MultiplyNumbers): string | undefined {
    return `Can’t display number content types. Number was ${JSON.stringify(
      content
    )}`
    // return undefined to indicate that this content type should not be displayed if it's not supported by a client
  }
 
  // Set to true to enable push notifications for interoperable content types.
  // Receiving clients must handle this field appropriately.
  shouldPush(): boolean {
    return true;
  }
}

Configure the content type

Import and register the custom content type.

Browser
import { ContentTypeMultiplyNumberCodec } from "./xmtp-content-type-multiply-number";
 
const client = await Client.create({
  env: "production",
  codecs: [new ContentTypeMultiplyNumberCodec()],
});
//or
client.registerCodec(new ContentTypeMultiplyNumberCodec());

Send the content

Send a message using the custom content type. This code sample demonstrates how to use the MultiplyCodec custom content type to perform multiplication operations.

Browser
const numbersToMultiply = new MultiplyNumbers(2, 3);
 
conversation.send(numbersToMultiply, {
  contentType: ContentTypeMultiplyNumbers,
});

Receive the content

To use the result of the multiplication operation, add a renderer for the custom content type.

To handle unsupported content types, see the fallback section.

Browser
if (message.contentType.sameAs(ContentTypeMultiplyNumber)) {
  return message.content; // 21
}

Advanced: Build a custom content type to send a transaction hash

This tutorial covers how to build a custom content type that sends transaction hashes on the Polygon blockchain. This example also describes how to use the custom content type to render the transaction hash.

Create the custom content type

Create a new file, xmtp-content-type-transaction-hash.tsx. This file hosts the TransactionHash class for encoding and decoding the custom content type.

Browser
import { ContentTypeId } from "@xmtp/xmtp-js";
 
export const ContentTypeTransactionHash = new ContentTypeId({
  authorityId: "your.domain",
  typeId: "transaction-hash",
  versionMajor: 1,
  versionMinor: 0,
});
 
export class ContentTypeTransactionHashCodec {
  get contentType() {
    return ContentTypeTransactionHash;
  }
 
  encode(hash) {
    return {
      type: ContentTypeTransactionHash,
      parameters: {},
      content: new TextEncoder().encode(hash),
    };
  }
 
  decode(content: { content: any }) {
    const uint8Array = content.content;
    const hash = new TextDecoder().decode(uint8Array);
    return hash;
  }
}

Import and register the custom content type

Browser
import {
  ContentTypeTransactionHash,
  ContentTypeTransactionHashCodec,
} from "./xmtp-content-type-transaction-hash";
 
const xmtp = await Client.create(signer, {
  env: "dev",
});
xmtp.registerCodec(new ContentTypeTransactionHashCodec());

Send a message using the custom content type

This code sample demonstrates how to use the TransactionHash content type to send a transaction.

Browser
// Create a wallet from a known private key
const wallet = new ethers.Wallet(privateKey);
console.log(`Wallet address: ${wallet.address}`);
 
//im using a burner wallet with MATIC from a faucet
//https://faucet.polygon.technology/
 
// Set up provider for Polygon Testnet (Mumbai)
const provider = new ethers.providers.JsonRpcProvider(
  "https://rpc-mumbai.maticvigil.com",
);
 
// Connect the wallet to the provider
const signer = wallet.connect(provider);
 
// Define the recipient address and amount
const amount = ethers.utils.parseEther("0.01"); // Amount in ETH (0.01 in this case)
 
// Create a transaction
const transaction = {
  to: recipientAddress,
  value: amount,
};
 
// Sign and send the transaction
const tx = await signer.sendTransaction(transaction);
console.log(`Transaction hash: ${tx.hash}`);
 
const conversation = await xmtp.conversations.newConversation(WALLET_TO);
await conversation
  .send(tx.hash, {
    contentType: ContentTypeTransactionHash,
  })
  .then(() => {
    console.log("Transaction data sent", tx.hash);
  })
  .catch((error) => {
    console.log("Error sending transaction data: ", error);
  });

Use the result of the hash

Add an async renderer for the custom content type.

Browser
if (message.contentType.sameAs(ContentTypeTransactionHash)) {
  // Handle ContentTypeAttachment
  return (
    <TransactionMonitor key={message.id} encodedContent={message.content} />
  );
}
 
const TransactionMonitor = ({ encodedContent }) => {
  const [retryCount, setRetryCount] = useState(0);
 
  const [transactionValue, setTransactionValue] = useState(null);
 
  useEffect(() => {
    const fetchTransactionReceipt = async () => {
      console.log(encodedContent);
      const provider = new ethers.providers.JsonRpcProvider(
        "https://rpc-mumbai.maticvigil.com",
      );
      const receipt = await provider.getTransactionReceipt(encodedContent);
      const tx = await provider.getTransaction(encodedContent);
      if (tx && tx.value) {
        setTransactionValue(ethers.utils.formatEther(tx.value));
      }
    };
    fetchTransactionReceipt();
  }, [encodedContent, retryCount]);
 
  return transactionValue ? (
    <div>Transaction value: {transactionValue} ETH</div>
  ) : (
    <div>
      Waiting for transaction to be mined...
      <button onClick={() => setRetryCount(retryCount + 1)}>
        Refresh Status 🔄
      </button>
    </div>
  );
};