I’m creating example code that can help guide my app-builder build a chat app. Sending and receiving messages via dev network works smoothly. However I am having some issues with production I need help with.
When I send a message from my built app to the recipient in xmtp.chat (who is in production). It says a the message has been sent but my address on the XMTP chat site doesn’t receive it, and also when i send a message from the recipient (in xmtp.chat) to the sender (in my built app) the message doesn’t come through either.
I did some trouble shooting with some console logs to figure out what could be causing this. I have arrived at 2 possible causes.
1- Even though the user selects production to connect their wallet and initiate the client in production, the handleSyncUrl seems to be reverting to (…history.dev.ephemera.network/) instead of (…production.ephemera.network). I setup my code to make sure the handleSyncUrl corresponds to the user’s network selection but somehow it is not working. So despite setting the environment to production in my app, the XMTP client seems to be connecting to the dev network
2- The production environment seems to require consent. And possibly there are consent mismatches between the the sender and recipient. The Policy Standard has a Deny error, which is preventing group membership updates.
CONSOLE ERROR: - INFO sync_until_intent_resolved:sync_with_conn: xmtp_mls::groups::group_permissions: Policy Standard(Deny) failed for actor CommitParticipant { inbox_id=5a7b2b2696688ce5212d9672b76bd29dd71c6cdda721a5abc362aeee08a2b622, … } and change Inbox { inbox_id: “b64cfca45e90bbec36fa4defa1fa8af97cac840ecb6b6e515e29cb43bd5eec03”, … }
This is happening even though i have set my recipient consent to “Allowed” inside xmtp chat.
In my console logs when using my app it confirms that the client is initialiased with production network, so i’m not sure why I am getting these two errors.
LOGS:
- Connecting to XMTP with env: production
- initClient called with env: production
- XMTP Client initialized: env=“production”, historySyncUrl=“…message-history.production.ephemera.network” //i had to remove the https because i’m only allowed to post 2 links
Unless of course there is something wrong with how I am setting them up.
Unfortunately I don’t have a full repo of this, since it is merely an example guide to be used by an AI to help guide it build an app. So I can only show the mock code I have and not a full repo. I am attaching code for my wallet connection and XMTPProvider as reference, since these are the sections where the connections and client initializations are taking place.
WalletConnect:
import { useState } from 'react';
import type { Signer } from '@xmtp/browser-sdk';
import { useXMTPClient } from '@/providers/XMTPProvider';
import { createWalletClient, custom, http } from 'viem';
import { mainnet } from 'viem/chains';
export function WalletConnect({ onConnect }: { onConnect?: () => void }) {
const { initClient } = useXMTPClient();
const [isConnecting, setIsConnecting] = useState<boolean>(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string>('');
const [address, setAddress] = useState<string>('');
const [env, setEnv] = useState<'dev' | 'production' | 'local'>(
(localStorage.getItem('xmtpEnv') as 'dev' | 'production' | 'local') || 'dev'
)
useEffect(() => {
localStorage.setItem('xmtpEnv', env); // Save to localStorage whenever env changes
}, [env]);
// Create a viem wallet client. Using `custom` to wrap window.ethereum.
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum),
});
const createSigner = (
address: `0x${string}`,
walletClient: WalletClient
): Signer => {
return {
type: 'EOA',
getIdentifier: () => ({
identifier: address.toLowerCase(),
identifierKind: 'Ethereum',
}),
signMessage: async (message: string) => {
const signature = await walletClient.signMessage({
account: address,
message,
});
return toBytes(signature);
},
};
};
const connectWallet = async () => {
setIsConnecting(true);
try {
// Request accounts via viem wallet client
const accounts: string[] = await walletClient.request({
method: 'eth_requestAccounts',
params: [],
});
if (accounts && accounts.length > 0) {
const accountAddress = accounts[0];
setAddress(accountAddress);
setSuccessMessage(`Wallet Connected, please confirm signing with wallet`);
// Create an XMTP signer using the viem-based helper
const xmtpSigner: Signer = createSigner(accountAddress, walletClient);
console.log('Connecting to XMTP with env:', env);
setSuccessMessage(`Connecting to XMTP with env: ${env}`);
await initClient(xmtpSigner, env);
if (onConnect) {
onConnect(); // Call onConnect only if it's provided
}
} else {
throw new Error('No accounts returned');
}
} catch (err) {
console.error('Wallet connection error:', err);
setErrorMsg(
err instanceof Error ? err.message : 'Failed to connect wallet'
);
} finally {
setIsConnecting(false);
}
};
// Optionally, auto-connect when address becomes available.
useEffect(() => {
if (address && client) {
setSuccessMessage('XMTP client initialized successfully!');
}
// Only auto-connect if client is null and address is not set
if (!client && !address && window.ethereum) {
connectWallet();
}
}, [address, client]);
return (...
<label htmlFor='env-select' className='block text-sm font-medium text-gray-700 mb-1'>
Select Network
</label>
<select
id='env-select'
value={env}
onChange={(e) => setEnv(e.target.value as 'dev' | 'production' | 'local')}
className='w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-600 text-black'
>
<option value='dev'>dev</option>
<option value='production'>production</option>
<option value='local'>local</option>
</select>
</div>
<button
onClick={connectWallet}
disabled={isConnecting}
className='bg-blue-600 text-white px-4 py-2 rounded'
>
{isConnecting ? 'Connecting...' : 'Connect Wallet'}
</button>
....```
XMTPProvider Setup:
‘use client’;
import { createContext, useCallback, useContext, useState, type ReactNode } from ‘react’;
import { Client, Signer } from ‘@xmtp/browser-sdk’;
interface XMTPContextType {
client: Client | null;
isLoading: boolean;
error: string | null;
walletAddress: string;
initClient: (signer: Signer, env: ‘dev’ | ‘production’ | ‘local’) => Promise;
disconnect: () => void; // Add disconnect met
}
const XMTPContext = createContext({
client: null,
isLoading: false,
error: null,
walletAddress: ‘’,
initClient: (signer: Signer, env: ‘dev’ | ‘production’ | ‘local’) => Promise; // Updated to accept env
disconnect: () => {}, // Default empty function
});
export const HistorySyncUrls = {
local: //only allowed to post 2 links so I removed this
dev: “https://message-history.dev.ephemera.network”,
production: “https://message-history.production.ephemera.network”,
} as const;
export function XMTPProvider({ children }: { children: ReactNode }) {
const [walletAddress, setWalletAddress] = useState(‘’);
const [client, setClient] = useState<Client | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const initClient = async (signer: Signer, env: ‘dev’ | ‘production’ | ‘local’) => {
if (client) return; // Already initialized
setIsLoading(true);
setError(null);
try {
const identifier = await signer.getIdentifier();
const address =
identifier.identifierKind === ‘Ethereum’
? identifier.identifier
: undefined;
setWalletAddress(address);
const encryptionKey = window.crypto.getRandomValues(new Uint8Array(32));
// Log incoming env
console.log(`initClient called with env: ${env}`);
// Close existing client if environment changes
if (client) {
await client.close();
setClient(null);
}
if (!client) {
const xmtpClient = await Client.create(
signer,
encryptionKey,
{ env,
historySyncUrl: HistorySyncUrls[env]
});
console.log('XMTP Client initialized:', { env, historySyncUrl: HistorySyncUrls[env], clientEnv: xmtpClient.env });
setClient(xmtpClient);
}
} catch (err) {
console.error('Error initializing XMTP client:', err);
setError(
err instanceof Error ? err.message : 'Failed to initialize XMTP client'
);
} finally {
setIsLoading(false);
}
};
const disconnect = useCallback(async () => {
if (client) {
await client.close();
setClient(null);
setWalletAddress(null);
setError(null);
}
}, [client]);
return (
<XMTPContext.Provider
value={{
client,
isLoading,
error,
walletAddress,
initClient,
disconnect,
}}
>
{children}
</XMTPContext.Provider>
);
}
export function useXMTPClient() {
const context = useContext(XMTPContext);
if (!context) {
throw new Error(‘useXMTPClient must be used within an XMTPProvider’);
}
return context;
}
Some additional observations I get from troubleshooting this are:
"The clientEnv: undefined in the XMTP Client initialized log suggests that the @xmtp/browser-sdk might not expose the env property on the client object as expected, or there’s a mismatch in how the SDK interprets the env option.
The NewConversation.tsx:75 Created DM with inboxId: b64cfca45e90bbec36fa4defa1fa8af97cac840ecb6b6e515e29cb43bd5eec03 Consent state: 0 log which indicates the recipient’s consent state is unknown (likely represented as 0), meaning they haven’t approved or denied messages yet. Your NewConversation component warns about this, but the group creation fails due to the Deny policy."
but I started a conversation on the XMTP chat side with the address i'm using in my app build so i'm not sure why this is the case.
I can't seem to figure out what I am doing wrong here so i'm hoping to get some help and guidance on how best to move forward.
For additional insight I am adding the codes for my 2 other module/components which create conversations and send messages, in case that helps understanding what i'm looking to achieve here:
NEW CONVERSATION:
‘use client’;
import { useState } from ‘react’;
import { useXMTPClient } from ‘@/providers/XMTPProvider’;
import { Dm } from ‘@xmtp/browser-sdk’;
interface NewConversationProps {
onClose: () => void;
onConversationCreated: (
newConvo: Group | Dm,
conversationName?: string
) => void;
}
export function NewConversation({
onClose,
onConversationCreated,
}: NewConversationProps) {
const { client } = useXMTPClient();
const [address, setAddress] = useState(‘’); // Renamed from inboxId to address for clarity
const [error, setError] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [conversationName, setConversationName] = useState(‘’);
const [retryCount, setRetryCount] = useState(0);
const maxRetries = 3;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setIsCreating(true);
try {
if (!client) {
setError(‘Client not initialized’);
throw new Error(
‘XMTP client not initialized. Please check your connection.’
);
}
const trimmedAddress = address.trim();
if (!trimmedAddress) {
throw new Error(‘Please enter a recipient address.’);
}
// Construct an Identifier object
const identifier = {
identifier: trimmedAddress.toLowerCase(),
identifierKind: 'Ethereum',
};
// Check if the address can receive messages
const canMessage = await client.canMessage([identifier]);
if (!canMessage.get(trimmedAddress.toLowerCase())) {
throw new Error(
`Recipient ${trimmedAddress} is not enabled for XMTP messages.`
);
}
// Resolve the address to an inboxId
const inboxId = await client.findInboxIdByIdentifier(identifier);
if (!inboxId) {
throw new Error(
`No inbox ID found for address ${trimmedAddress}. The user may not be registered on XMTP.`
);
}
// Check recipient's consent state
const consentState = await client.preferences.getConsentState('inbox_id', inboxId);
console.log('Recipient Consent state:', consentState);
if (consentState === 'denied') {
throw new Error(`Recipient ${trimmedAddress} has denied messages from you. Please request permission.`);
}
// Set sender's consent to allowed for recipient
await client.preferences.setConsentStates([{
entityType: 'inbox_id',
entity: inboxId,
state: 'allowed',
}]);
// Create a new DM with retry logic
const attemptCreateConversation = async (
attempt: number
): Promise<Dm> => {
try {
const conversation = await client.conversations.newDm(inboxId);
await conversation.sync(); // Ensure group is synced
console.log('Created DM with inboxId:', inboxId, 'Consent state:', consentState);
return conversation;
} catch (err) {
console.error(`Attempt ${attempt} failed:`, err);
if (attempt < maxRetries) {
// Update retry count for user feedback
setRetryCount(attempt + 1);
// Wait before retrying (exponential backoff)
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await new Promise((resolve) => setTimeout(resolve, delay));
return attemptCreateConversation(attempt + 1);
}
throw err; // If max retries reached, throw the error
}
};
const conversation = await attemptCreateConversation(0);
onConversationCreated(conversation, conversationName);
if (consentState === 'unknown') {
setError('Warning: Recipient has not approved messages. The conversation is created, but messages may not be delivered until they approve.');
}
onClose(); // Close the modal on success
} catch (err) {
console.error('Error starting conversation:', err);
let errorMessage =
err instanceof Error ? err.message : 'Failed to start conversation';
if (
err instanceof Error &&
err.message.includes('error sending request')
) {
errorMessage =
'Failed to connect to XMTP network. Please check your connection and try again.';
}
setError(errorMessage);
} finally {
setIsCreating(false); // Ensure this runs to reset the button state
setRetryCount(0);
}
};
return (
New Conversation
Close
<input
type=‘text’
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder=‘Enter recipient wallet address’
className=‘w-full p-2 border text-black rounded mb-2’
required
disabled={isCreating}
/>
Conversation Name (Optional)
<input
type=‘text’
id=‘conversationName’
value={conversationName}
onChange={(e) => setConversationName(e.target.value)}
className=‘mt-1 text-black block w-full border border-gray-300 rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-blue-600’
placeholder=‘Enter conversation name (optional)’
disabled={isCreating}
/>
{error &&
{error}
}{isCreating && (
Creating conversation…{’ '}
{retryCount > 0 ? (Retry ${retryCount}/${maxRetries})
: ‘’}
)}
{isCreating ? ‘Creating…’ : ‘Start Conversation’}
);
}
MESSAGE COMPOSER:
‘use client’;
import { useState } from ‘react’;
import { useXMTPClient } from ‘@/providers/XMTPProvider’;
import { Group, Dm } from ‘@xmtp/browser-sdk’;
export default function MessageComposer({
conversation,
onMessageSent, // optional callback when a message is successfully sent
}: {
conversation: Group | Dm;
onMessageSent?: (messageId: string) => void;
}) {
const { client } = useXMTPClient();
const [message, setMessage] = useState(‘’);
const [status, setStatus] = useState(‘’);
const maxRetries = 3;
const handleSend = async () => {
if (!message.trim()) {
setStatus(‘Message cannot be empty’);
return;
}
setStatus(‘Sending…’);
try {
if (!client) {
throw new Error(‘XMTP client is not initialized’);
}
if (!conversation) {
throw new Error(‘No conversation selected’);
}
// Sync the conversation to ensure group state is up-to-date
await conversation.sync();
// Validate recipient for DMs on production network
let consentState: string | undefined;
if (conversation instanceof Dm) {
const peerInboxId = await conversation.peerInboxId();
// Check recipient's consent state
const consentState = await client.preferences.getConsentState('inbox_id', peerInboxId);
if (consentState === 'denied') {
throw new Error(`Recipient ${peerInboxId} has denied messages from you. Please request permission.`);
}
if (consentState === 'unknown') {
setStatus('Warning: Recipient has not approved messages. Sending anyway, but delivery is not guaranteed.');
}
}
// Send message with retry logic
const attemptSend = async (attempt: number): Promise<string> => {
try {
const messageId = await conversation.send(message);
await conversation.sync();
return messageId;
} catch (err) {
console.error(`Send attempt ${attempt} failed:`, err);
if (attempt < maxRetries) {
await conversation.sync();
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
return attemptSend(attempt + 1);
}
throw err;
}
};
const messageId = await attemptSend(0);
setMessage(''); // Clear input
setStatus(`Message sent${consentState === 'unknown' ? ' (delivery not guaranteed)' : ''}`);
if (onMessageSent) {
onMessageSent(messageId);
}
} catch (error) {
console.error('Error sending message:', error);
const errorMessage = error instanceof Error
? error.message.includes('not enabled') || error.message.includes('denied')
? error.message
: `Failed to send message: ${error.message}`
: 'Failed to send message';
setStatus('Failed to send message.');
}
};
return (
<input
type=‘text’
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder=‘Type your message here’
className=‘flex-1 p-2 border rounded !text-black’
/>
Send
{status &&
{status}
});
}