Skip to main content
Sign arbitrary messages with a user’s embedded Ethereum wallet using the personal_sign method. Message signing is commonly used for authentication, proof of ownership, and off-chain authorization.
This method uses Ethereum’s personal_sign RPC method, which prefixes messages with "\x19Ethereum Signed Message:\n" before signing. For raw hash signatures, see Sign Raw Hash.

Overview

The useSignMessage hook from MoonKey’s React SDK provides a simple interface for signing messages. Common use cases include:
  • User authentication: Prove wallet ownership without passwords
  • Off-chain authorization: Sign permissions without gas fees
  • Proof of ownership: Verify control of an Ethereum address
  • Session tokens: Generate authenticated session credentials

React SDK

  • Basic Usage
  • With Callbacks
  • With UI Customization
To sign a message, use the signMessage method from the useSignMessage hook:
import { useMoonKey } from '@moon-key/react-auth';
import { useSignMessage } from '@moon-key/react-auth/ethereum';

function SignMessageButton() {
  const { user } = useMoonKey();
  const { signMessage } = useSignMessage();

  const handleSign = async () => {
    if (!user?.wallet) {
      alert('No wallet found');
      return;
    }

    try {
      const result = await signMessage({
        message: 'Welcome to My dApp! Sign this message to authenticate.'
      });

      console.log('Message signed:', result.signature);
      alert('Message signed successfully!');
    } catch (error) {
      console.error('Signing failed:', error);
    }
  };

  return (
    <button onClick={handleSign}>
      Sign Message
    </button>
  );
}

Parameters

Message Object

The first parameter to signMessage:
message
string
required
The message to sign with the wallet. Can be any string.Example:
message: 'Welcome to My dApp! Please sign to authenticate.'

Options Object

The second parameter is an optional configuration object:
uiConfig
object
Configuration for the signing confirmation modal UI.
wallet
Wallet
Specific wallet to use for signing. If not provided, uses the user’s default wallet.Example:
wallet: user.wallet

Hook Callbacks

Configure callbacks when initializing the hook:
onSuccess
(result: { signature: string }) => void
Callback executed after a user successfully signs a message. Receives the signature.Example:
const { signMessage } = useSignMessage({
  onSuccess: ({ signature }) => {
    console.log('Signed:', signature);
    submitToBackend(signature);
  }
});
onError
(error: Error) => void
Callback executed if signing fails or user cancels. Receives the error.Example:
const { signMessage } = useSignMessage({
  onError: (error) => {
    console.error('Failed:', error);
    showErrorNotification(error.message);
  }
});

Returns

The signMessage method returns a Promise that resolves with:
signature
string
The hex-encoded signature produced by the wallet.Example:
"0x9c29f0c3a6f3e55f8f7b5e9f8c1b8e2a4d5c6b7a8e9f0c1b2a3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1b"
encoding
'hex'
The encoding format of the signature. Currently always 'hex' for Ethereum.

Complete Examples

User Authentication

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSignMessage } from '@moon-key/react-auth/ethereum';
import { useState } from 'react';

export default function AuthenticateWithSignature() {
  const { user, isAuthenticated } = useMoonKey();
  const [isAuthenticating, setIsAuthenticating] = useState(false);
  const [authToken, setAuthToken] = useState<string | null>(null);

  const { signMessage } = useSignMessage({
    onSuccess: async ({ signature }) => {
      console.log('Signature obtained:', signature);

      // Send signature to backend for verification
      try {
        const response = await fetch('/api/auth/verify', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            address: user?.wallet?.address,
            signature,
            message: 'Sign in to My dApp'
          })
        });

        const data = await response.json();
        setAuthToken(data.token);
        alert('Authentication successful!');
      } catch (error) {
        console.error('Backend verification failed:', error);
        alert('Authentication failed');
      } finally {
        setIsAuthenticating(false);
      }
    },
    onError: (error) => {
      console.error('Signing failed:', error);
      alert('Authentication cancelled or failed');
      setIsAuthenticating(false);
    }
  });

  const handleAuthenticate = async () => {
    if (!user?.wallet) {
      alert('Please connect your wallet first');
      return;
    }

    setIsAuthenticating(true);

    await signMessage({
      message: 'Sign in to My dApp'
    }, {
      uiConfig: {
        title: 'Sign In',
        description: 'Sign this message to authenticate with My dApp',
        confirmButtonText: 'Sign In'
      }
    });
  };

  if (!isAuthenticated) {
    return <p>Please sign in first</p>;
  }

  return (
    <div className="auth-container">
      <h2>Wallet Authentication</h2>
      
      {!authToken ? (
        <button
          onClick={handleAuthenticate}
          disabled={isAuthenticating}
          className="auth-button"
        >
          {isAuthenticating ? 'Authenticating...' : 'Sign to Authenticate'}
        </button>
      ) : (
        <div className="authenticated">
          <p>✅ Authenticated</p>
          <p className="token">Token: {authToken.slice(0, 20)}...</p>
        </div>
      )}
    </div>
  );
}

Session Authorization

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSignMessage } from '@moon-key/react-auth/ethereum';
import { useState, useEffect } from 'react';

export default function SessionAuthorization() {
  const { user } = useMoonKey();
  const [sessionToken, setSessionToken] = useState<string | null>(null);
  const [expiresAt, setExpiresAt] = useState<Date | null>(null);

  const { signMessage } = useSignMessage({
    onSuccess: async ({ signature }) => {
      // Create session on backend
      try {
        const response = await fetch('/api/session/create', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            address: user?.wallet?.address,
            signature,
            timestamp: Date.now()
          })
        });

        const data = await response.json();
        setSessionToken(data.sessionToken);
        setExpiresAt(new Date(data.expiresAt));

        // Store in localStorage
        localStorage.setItem('sessionToken', data.sessionToken);
      } catch (error) {
        console.error('Session creation failed:', error);
      }
    }
  });

  const createSession = async () => {
    if (!user?.wallet) return;

    const timestamp = Date.now();
    const message = `Create session at ${timestamp}`;

    await signMessage({
      message
    }, {
      uiConfig: {
        title: 'Create Session',
        description: 'Sign to create an authenticated session',
        confirmButtonText: 'Create Session'
      }
    });
  };

  const revokeSession = () => {
    setSessionToken(null);
    setExpiresAt(null);
    localStorage.removeItem('sessionToken');
  };

  // Check for existing session on mount
  useEffect(() => {
    const stored = localStorage.getItem('sessionToken');
    if (stored) {
      setSessionToken(stored);
    }
  }, []);

  return (
    <div className="session-auth">
      <h2>Session Authorization</h2>

      {!sessionToken ? (
        <div>
          <p>No active session</p>
          <button onClick={createSession}>
            Create Session
          </button>
        </div>
      ) : (
        <div className="active-session">
          <p>✅ Session Active</p>
          <p className="session-token">Token: {sessionToken.slice(0, 20)}...</p>
          {expiresAt && (
            <p className="expires">Expires: {expiresAt.toLocaleString()}</p>
          )}
          <button onClick={revokeSession} className="revoke-button">
            Revoke Session
          </button>
        </div>
      )}
    </div>
  );
}

Proof of Ownership

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSignMessage } from '@moon-key/react-auth/ethereum';
import { useState } from 'react';
import { verifyMessage } from 'viem';

export default function ProofOfOwnership() {
  const { user } = useMoonKey();
  const [signature, setSignature] = useState<string>('');
  const [isVerified, setIsVerified] = useState<boolean | null>(null);

  const { signMessage } = useSignMessage({
    onSuccess: ({ signature }) => {
      setSignature(signature);
      console.log('Signature:', signature);
    }
  });

  const handleProve = async () => {
    if (!user?.wallet) return;

    const message = `I own address ${user.wallet.address}`;

    await signMessage({
      message
    }, {
      uiConfig: {
        title: 'Prove Ownership',
        description: 'Sign to prove you own this wallet address',
        confirmButtonText: 'Sign Proof'
      }
    });
  };

  const handleVerify = async () => {
    if (!signature || !user?.wallet) return;

    try {
      const message = `I own address ${user.wallet.address}`;

      // Verify signature using viem
      const isValid = await verifyMessage({
        address: user.wallet.address as `0x${string}`,
        message,
        signature: signature as `0x${string}`
      });

      setIsVerified(isValid);
    } catch (error) {
      console.error('Verification failed:', error);
      setIsVerified(false);
    }
  };

  return (
    <div className="proof-ownership">
      <h2>Prove Wallet Ownership</h2>

      <div className="wallet-info">
        <p>Wallet: {user?.wallet?.address}</p>
      </div>

      <button onClick={handleProve} disabled={!user?.wallet}>
        Generate Proof
      </button>

      {signature && (
        <div className="signature-section">
          <h3>Signature Generated</h3>
          <p className="signature">{signature.slice(0, 40)}...</p>

          <button onClick={handleVerify}>
            Verify Signature
          </button>

          {isVerified !== null && (
            <p className={isVerified ? 'verified' : 'invalid'}>
              {isVerified ? '✅ Signature Valid' : '❌ Signature Invalid'}
            </p>
          )}
        </div>
      )}
    </div>
  );
}

Verifying Signatures

After obtaining a signature, you can verify it using viem:
import { verifyMessage } from 'viem';

async function verifySignature(
  address: string,
  message: string,
  signature: string
): Promise<boolean> {
  try {
    const isValid = await verifyMessage({
      address: address as `0x${string}`,
      message,
      signature: signature as `0x${string}`
    });

    return isValid;
  } catch (error) {
    console.error('Verification failed:', error);
    return false;
  }
}

// Usage
const isValid = await verifySignature(
  '0xUserWalletAddress...',
  'Welcome to My dApp!',
  '0x9c29f0c3a6f3e55f8f7b5e9f...'
);

if (isValid) {
  console.log('Signature is valid!');
} else {
  console.log('Signature is invalid!');
}

Backend Verification (Node.js)

// backend/verify.ts
import { verifyMessage } from 'viem';

export async function verifyUserSignature(
  address: string,
  message: string,
  signature: string
): Promise<{ valid: boolean; error?: string }> {
  try {
    const isValid = await verifyMessage({
      address: address as `0x${string}`,
      message,
      signature: signature as `0x${string}`
    });

    return { valid: isValid };
  } catch (error: any) {
    return {
      valid: false,
      error: error.message
    };
  }
}

// API route example
app.post('/api/verify-signature', async (req, res) => {
  const { address, message, signature } = req.body;

  const result = await verifyUserSignature(address, message, signature);

  if (result.valid) {
    // Generate JWT or session token
    const token = generateAuthToken(address);
    res.json({ success: true, token });
  } else {
    res.status(401).json({ success: false, error: result.error });
  }
});

UI Customization

Customize the signing modal to match your brand:
import { useSignMessage } from '@moon-key/react-auth/ethereum';

const { signMessage } = useSignMessage();

await signMessage({
  message: 'Sign this message'
}, {
  uiConfig: {
    title: 'Verify Your Identity',
    description: 'Sign this message to prove wallet ownership',
    confirmButtonText: 'Sign Message',
    cancelButtonText: 'Cancel'
  }
});

Hide Modal

For silent signing (ensure user has given explicit consent):
await signMessage({
  message: 'Background signature'
}, {
  uiConfig: {
    showWalletUI: false
  }
});
Hiding the signing modal removes the user’s ability to review the message before signing. Only use this for trusted operations or after obtaining explicit user consent.

Message Formatting Best Practices

Always make messages understandable to users:
// ✅ Good - Clear and specific
await signMessage({
  message: 'Sign in to My dApp on December 15, 2024'
});

// ❌ Bad - Cryptic or unclear
await signMessage({
  message: 'auth:1234567890:xyz'
});
Explain what the signature will be used for:
await signMessage({
  message: `Sign this message to authenticate with My dApp.
  
Address: ${walletAddress}
Timestamp: ${Date.now()}
Action: Login

This signature will not cost any gas fees.`
});
Include timestamps or nonces to prevent signature replay:
const timestamp = Date.now();
const nonce = generateNonce();

await signMessage({
  message: `Authenticate at ${timestamp} with nonce ${nonce}`
});

// Verify timestamp is recent on backend
const isRecent = Date.now() - timestamp < 60000; // 1 minute
Include your app’s domain to prevent phishing:
await signMessage({
  message: `Sign in to myapp.com
  
This request is from: https://myapp.com
Time: ${new Date().toISOString()}
Action: Authentication`
});
For structured data, consider using EIP-712 (Sign Typed Data):
// For simple auth, use personal_sign
await signMessage({ message: 'Sign in to My App' });

// For complex structured data, use signTypedData instead
// See: /wallet-as-a-service/using-wallets/ethereum/sign-typed-data

Error Handling

Handle common signing errors gracefully:
import { useMoonKey } from '@moon-key/react-auth';
import { useSignMessage } from '@moon-key/react-auth/ethereum';

function SignWithErrorHandling() {
  const { user } = useMoonKey();
  
  const { signMessage } = useSignMessage({
    onSuccess: ({ signature }) => {
      console.log('Success:', signature);
      // Handle success
    },
    onError: (error: any) => {
      if (error.code === 'USER_REJECTED') {
        console.log('User cancelled signing');
        // Don't show error - user deliberately cancelled
      } else if (error.code === 'INVALID_MESSAGE') {
        console.log('Invalid message format');
        alert('The message format is invalid');
      } else if (error.code === 'NETWORK_ERROR') {
        console.log('Network error during signing');
        alert('Network error. Please try again');
      } else {
        console.error('Signing failed:', error);
        alert('Failed to sign message: ' + error.message);
      }
    }
  });

  const handleSign = async () => {
    if (!user?.wallet) return;

    await signMessage({
      message: 'Sign this message'
    });
  };

  return <button onClick={handleSign}>Sign Message</button>;
}

Best Practices

Never trust client-side signature verification alone:
// ❌ Bad - Only client-side verification
const isValid = await verifyMessage({ address, message, signature });
if (isValid) {
  setAuthenticated(true); // Insecure!
}

// ✅ Good - Verify on backend
const response = await fetch('/api/verify-signature', {
  method: 'POST',
  body: JSON.stringify({ address, message, signature })
});
const { valid, token } = await response.json();
if (valid) {
  setAuthToken(token); // Secure!
}
If storing signatures, ensure proper security:
// ❌ Don't store in localStorage without encryption
localStorage.setItem('signature', signature);

// ✅ Store session tokens instead
localStorage.setItem('sessionToken', jwtToken);

// ✅ Or store on secure backend
await fetch('/api/sessions', {
  method: 'POST',
  body: JSON.stringify({ signature, address })
});
Add expiration to prevent old signatures from being reused:
const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes
const message = `Sign in to My App

Expires: ${expiresAt}
Address: ${address}`;

// Backend verification
if (Date.now() > expiresAt) {
  return { valid: false, error: 'Signature expired' };
}
Keep users informed throughout the signing process:
const [status, setStatus] = useState<'idle' | 'signing' | 'verifying' | 'success' | 'error'>('idle');

const handleSign = async () => {
  setStatus('signing');

  const { signature } = await signMessage({ message: 'Sign in' });

  setStatus('verifying');

  const response = await fetch('/api/verify', {
    method: 'POST',
    body: JSON.stringify({ signature })
  });

  if (response.ok) {
    setStatus('success');
  } else {
    setStatus('error');
  }
};
Don’t show errors when users deliberately cancel:
const { signMessage } = useSignMessage({
  onError: (error: any) => {
    if (error.code === 'USER_REJECTED') {
      // User cancelled - this is expected behavior
      console.log('User cancelled signing');
      return;
    }
    // Show error for other cases
    alert('Signing failed: ' + error.message);
  }
});

Troubleshooting

Common causes and solutions:
  • Wrong message: Ensure the exact same message is used for verification
  • Address mismatch: Verify the correct wallet address is used
  • Encoding issues: Message must be UTF-8 encoded string
  • Wrong verification method: Use verifyMessage for personal_sign
// Ensure exact message match
const originalMessage = 'Sign in to My App';

// Sign
const { signature } = await signMessage({ message: originalMessage });

// Verify with EXACT same message
const isValid = await verifyMessage({
  address: walletAddress as `0x${string}`,
  message: originalMessage, // Must match exactly
  signature: signature as `0x${string}`
});
Handle cancellations without showing errors:
const { signMessage } = useSignMessage({
  onError: (error: any) => {
    if (error.code === 'USER_REJECTED' || error.message?.includes('User rejected')) {
      // Expected behavior - don't show error
      return;
    }
    // Show error for unexpected failures
    showErrorToast(error.message);
  }
});
Ensure proper message encoding:
// ✅ Use UTF-8 strings
await signMessage({
  message: 'Hello, world!' // UTF-8 string
});

// ❌ Don't pass raw bytes or hex
// await signMessage({
//   message: '0x48656c6c6f' // This won't work
// });

// For hex or bytes, decode first
const hexMessage = '0x48656c6c6f';
const decodedMessage = Buffer.from(hexMessage.slice(2), 'hex').toString('utf-8');
await signMessage({ message: decodedMessage });
Ensure callbacks are properly configured:
// ✅ Configure callbacks on hook initialization
const { signMessage } = useSignMessage({
  onSuccess: ({ signature }) => {
    console.log('Success:', signature);
  },
  onError: (error) => {
    console.log('Error:', error);
  }
});

// Then call signMessage
await signMessage({ message: 'Test' });

// ❌ Don't try to pass callbacks to signMessage directly
// await signMessage({ message: 'Test' }, { onSuccess: ... }); // Won't work

Next Steps