Skip to main content
The Offchain client enables interaction with traditional Snapshot spaces that operate entirely off-chain. It supports gasless voting, proposals, and space management through signed messages submitted to the Snapshot Sequencer.

Installation

npm install @snapshot-labs/sx

Client Types

The Offchain client comes in two implementations based on signature type:
  • OffchainEthereumSig: For Ethereum-based signatures (most common)
  • OffchainStarknetSig: For Starknet-based signatures

OffchainEthereumSig

The Ethereum signature client is the primary client for interacting with Snapshot offchain spaces.

Initialization

import { clients } from '@snapshot-labs/sx';

const client = new clients.OffchainEthereumSig({
  networkConfig: {
    eip712ChainId: 1 // Ethereum mainnet
  },
  sequencerUrl: 'https://seq.snapshot.org' // Optional, defaults to mainnet
});

Configuration

networkConfig
OffchainNetworkEthereumConfig
Network configuration for offchain signatures
sequencerUrl
string
URL of the Snapshot Sequencer. Defaults to:
  • Mainnet: https://seq.snapshot.org
  • Testnet: https://testnet.seq.snapshot.org

Proposal Methods

propose()

Create a new offchain proposal.
import { Wallet } from '@ethersproject/wallet';

const signer = new Wallet(privateKey);

const envelope = await client.propose({
  signer,
  data: {
    space: 'example.eth',
    type: 'single-choice',
    title: 'Should we implement feature X?',
    body: '## Overview\n\nThis proposal suggests...',
    discussion: 'https://forum.example.com/t/123',
    choices: ['For', 'Against', 'Abstain'],
    labels: [],
    start: Math.floor(Date.now() / 1000),
    end: Math.floor(Date.now() / 1000) + 604800, // 1 week
    snapshot: 18500000, // Block number
    plugins: JSON.stringify({}),
    app: 'snapshot'
  }
});

// Submit to Snapshot Sequencer
const result = await client.send(envelope);
console.log('Proposal ID:', result.id);
data
Propose
required
Proposal data object

updateProposal()

Update an existing proposal (before voting starts).
const envelope = await client.updateProposal({
  signer,
  data: {
    proposal: '0x...', // Proposal ID
    space: 'example.eth',
    type: 'single-choice',
    title: 'Updated: Should we implement feature X?',
    body: '## Updated Overview\n\n...',
    discussion: 'https://forum.example.com/t/123',
    choices: ['For', 'Against', 'Abstain'],
    labels: [],
    plugins: JSON.stringify({})
  }
});

const result = await client.send(envelope);

cancel()

Cancel a proposal (only by proposal author).
const envelope = await client.cancel({
  signer,
  data: {
    space: 'example.eth',
    proposal: '0x...' // Proposal ID
  }
});

const result = await client.send(envelope);

flagProposal()

Flag a proposal for moderation.
const envelope = await client.flagProposal({
  signer,
  data: {
    space: 'example.eth',
    proposal: '0x...'
  }
});

const result = await client.send(envelope);

Voting Methods

vote()

Cast a vote on a proposal. Supports multiple voting types.
const envelope = await client.vote({
  signer,
  data: {
    space: 'example.eth',
    proposal: '0x...',
    type: 'single-choice',
    choice: 1, // First option
    reason: 'I support this proposal because...',
    app: 'snapshot',
    privacy: 'none'
  }
});

const result = await client.send(envelope);
data.type
string
required
Vote type matching the proposal type
data.choice
number | number[] | Record<string, number>
required
Vote choice(s):
  • Single choice: 1 (first option)
  • Approval: [1, 3] (multiple options)
  • Ranked: [2, 1, 3] (ordered preferences)
  • Weighted/Quadratic: { 1: 60, 2: 40 } (distribution)
data.privacy
Privacy
required
Privacy setting: ‘none’ or ‘shutter’ (for encrypted votes)
data.reason
string
Optional voting reason/comment

Space Management

createSpace()

Create a new Snapshot space.
const envelope = await client.createSpace({
  signer,
  data: {
    space: 'mynewdao.eth',
    settings: JSON.stringify({
      name: 'My New DAO',
      network: '1',
      symbol: 'MND',
      strategies: [
        {
          name: 'erc20-balance-of',
          params: {
            address: '0x...',
            symbol: 'MND',
            decimals: 18
          }
        }
      ],
      admins: ['0x...'],
      members: [],
      plugins: {},
      voting: {
        delay: 0,
        period: 259200,
        type: 'single-choice',
        quorum: 0,
        privacy: 'none'
      }
    })
  }
});

const result = await client.send(envelope);

updateSpace()

Update space settings.
const envelope = await client.updateSpace({
  signer,
  data: {
    space: 'example.eth',
    settings: JSON.stringify({
      name: 'Updated DAO Name',
      about: 'Updated description',
      voting: {
        period: 432000 // 5 days
      }
    })
  }
});

const result = await client.send(envelope);

deleteSpace()

Delete a space (requires admin privileges).
const envelope = await client.deleteSpace({
  signer,
  data: {
    space: 'example.eth'
  }
});

const result = await client.send(envelope);

User Actions

followSpace()

Follow a space.
const envelope = await client.followSpace({
  signer,
  data: {
    space: 'example.eth',
    network: '1' // Ethereum mainnet
  }
});

const result = await client.send(envelope);

unfollowSpace()

Unfollow a space.
const envelope = await client.unfollowSpace({
  signer,
  data: {
    space: 'example.eth',
    network: '1'
  }
});

const result = await client.send(envelope);

setAlias()

Set an alias (ENS name) for your address.
const envelope = await client.setAlias({
  signer,
  data: {
    alias: 'myname.eth'
  }
});

const result = await client.send(envelope);

updateUser()

Update user profile.
const envelope = await client.updateUser({
  signer,
  data: {
    profile: JSON.stringify({
      name: 'John Doe',
      about: 'DAO enthusiast',
      avatar: 'ipfs://...'
    })
  }
});

const result = await client.send(envelope);

updateStatement()

Update delegate statement for a space.
const envelope = await client.updateStatement({
  signer,
  data: {
    space: 'example.eth',
    network: '1',
    about: 'Brief bio',
    statement: 'My full delegate statement...',
    discourse: 'username',
    status: 'active'
  }
});

const result = await client.send(envelope);

Message Submission

send()

Send any signed envelope to the Snapshot Sequencer.
const result = await client.send(envelope);

// Result contains:
// - id: Message ID
// - ipfsHash: IPFS hash of the message
// - relayerIpfsHash: Relayer's IPFS hash
result
object
Sequencer response

OffchainStarknetSig

The Starknet signature client enables Starknet users to participate in Snapshot offchain governance.
import { clients } from '@snapshot-labs/sx';
import { Account } from 'starknet';

const client = new clients.OffchainStarknetSig({
  networkConfig: {
    eip712ChainId: 1
  },
  sequencerUrl: 'https://seq.snapshot.org'
});

// Usage is similar to EthereumSig, but with Starknet Account
const account = new Account(provider, address, privateKey);

const envelope = await client.vote({
  signer: account,
  data: {
    space: 'example.eth',
    proposal: '0x...',
    type: 'single-choice',
    choice: 1,
    app: 'snapshot',
    privacy: 'none'
  }
});

const result = await client.send(envelope);

Type Definitions

Vote Types

type Choice = 
  | number                    // single-choice
  | number[]                  // approval, ranked-choice
  | Record<string, number>;   // weighted, quadratic

type Privacy = 'none' | 'shutter';

Envelope

type Envelope<T> = {
  signatureData?: SignatureData;
  data: T;
};

type SignatureData = {
  address: string;
  signature: string;
  domain: TypedDataDomain;
  types: Record<string, TypedDataField[]>;
  message: Record<string, any>;
};

Voting Privacy

Snapshot supports shielded voting through Shutter Network:
1

Enable Shutter

Set privacy: 'shutter' in your vote data
2

Encrypt Choice

Vote choice is automatically encrypted before submission
3

Voting Period

Votes remain encrypted during the voting period
4

Decryption

After voting ends, votes are decrypted and tallied
const envelope = await client.vote({
  signer,
  data: {
    space: 'example.eth',
    proposal: '0x...',
    type: 'single-choice',
    choice: 1,
    app: 'snapshot',
    privacy: 'shutter' // Encrypted vote
  }
});

Relayer Support

For gasless transactions on certain operations, set signature to 0x to use the relayer:
// Certain operations support gasless relaying
// The client automatically routes to relayer when sig is '0x'
const envelope = {
  signatureData: {
    signature: '0x',
    // ... other signature data
  },
  data: { /* ... */ }
};

const result = await client.send(envelope);
// Automatically routed to relayer.snapshot.org

Source Code

View the complete implementation:

Build docs developers (and LLMs) love