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 EIP-712 chain identifier: 1 for mainnet, 5 for Goerli testnet
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\n This 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 );
Proposal data object Space ID (e.g., ‘example.eth’)
Voting type: ‘single-choice’, ‘approval’, ‘ranked-choice’, ‘weighted’, ‘quadratic’
Proposal title (max 256 characters)
Proposal body in Markdown format
URL to discussion thread (can be empty string)
Voting start timestamp (Unix seconds)
Voting end timestamp (Unix seconds)
Block number for voting power snapshot
JSON stringified plugin configuration
Application identifier (e.g., ‘snapshot’)
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.
Single Choice Vote
Approval Vote
Weighted Vote
Ranked Choice Vote
Shielded Vote (Privacy)
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 );
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)
Privacy setting: ‘none’ or ‘shutter’ (for encrypted votes)
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
Sequencer response IPFS hash of the submitted message
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:
Enable Shutter
Set privacy: 'shutter' in your vote data
Encrypt Choice
Vote choice is automatically encrypted before submission
Voting Period
Votes remain encrypted during the voting period
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: