diff --git a/.gitignore b/.gitignore
index ccc356f..ead3d97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,9 @@ deployments/*.json
!deployments/allowlists/
!deployments/allowlists/*.json
+# Snapshot signed payloads (local only)
+space-message*.json
+
# Logs
*.log
npm-debug.log*
diff --git a/README.md b/README.md
index 4e1834c..700f6c7 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,11 @@ KEYSTORE_PATH=keystores/deployer-0x
.json
## Deployment
+### Deployment outputs
+
+- **Source of truth**: `config/chains.json`
+- **Local backups**: `deployments/*.json` (gitignored)
+
### Full Chain Deployment
Deploys all modules + aggregator:
@@ -97,6 +102,33 @@ pnpm exec hardhat verify --network
Get the aggregator address from `config/chains.json` → `chains..deployed.aggregator`.
+## Debugging Snapshot Settings
+
+To confirm what Snapshot Hub currently has stored for a space:
+
+```bash
+pnpm exec tsx scripts/check-snapshot-space.ts
+SPACE=quickvote.eth pnpm exec tsx scripts/check-snapshot-space.ts
+```
+
+## Publishing Snapshot Settings (Safe)
+
+When using Safe multi-sig, Snapshot space updates are signed as a Safe “message”, but the signed payload still needs to be broadcast to Snapshot Hub.
+
+1) Save the typed-data JSON you signed (the object with `domain`, `types`, `primaryType`, `message`) to a file, e.g. `space-message.json`.
+
+2) Publish it using the Safe prepared signature:
+
+```bash
+pnpm exec tsx scripts/publish-snapshot-settings.ts --file ./space-message.json --sig 0x
+```
+
+3) Verify it’s live:
+
+```bash
+pnpm exec tsx scripts/check-snapshot-space.ts
+```
+
## Testing
```bash
diff --git a/scripts/check-snapshot-space.ts b/scripts/check-snapshot-space.ts
new file mode 100644
index 0000000..161a4ab
--- /dev/null
+++ b/scripts/check-snapshot-space.ts
@@ -0,0 +1,77 @@
+/**
+ * Check Snapshot space settings (strategies) directly from Snapshot Hub.
+ *
+ * Usage:
+ * pnpm exec tsx scripts/check-snapshot-space.ts
+ * SPACE=quickvote.eth pnpm exec tsx scripts/check-snapshot-space.ts
+ *
+ * Notes:
+ * - Snapshot settings updates are off-chain. A Safe "message" being signed does NOT
+ * guarantee the settings were published to Snapshot Hub.
+ */
+
+type Strategy = { name: string; network: string; params: Record };
+
+const SPACE = process.env.SPACE || "quickvote.eth";
+const HUB_URL = process.env.SNAPSHOT_HUB_URL || "https://hub.snapshot.org/graphql";
+
+const query = `
+ query Space($id: String!) {
+ space(id: $id) {
+ id
+ name
+ network
+ strategies {
+ name
+ network
+ params
+ }
+ }
+ }
+`;
+
+function shortAddr(v: unknown): string {
+ if (typeof v !== "string") return String(v);
+ if (!v.startsWith("0x") || v.length < 10) return v;
+ return `${v.slice(0, 6)}…${v.slice(-4)}`;
+}
+
+async function main() {
+ const res = await fetch(HUB_URL, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ query, variables: { id: SPACE } }),
+ });
+
+ if (!res.ok) {
+ const text = await res.text().catch(() => "");
+ throw new Error(`Snapshot Hub request failed (${res.status}): ${text.slice(0, 300)}`);
+ }
+
+ const json = (await res.json()) as any;
+ if (json.errors?.length) {
+ throw new Error(`Snapshot Hub GraphQL error: ${JSON.stringify(json.errors, null, 2)}`);
+ }
+
+ const space = json.data?.space as
+ | { id: string; name: string; network: string; strategies: Strategy[] }
+ | null;
+ if (!space) throw new Error(`Space not found: ${SPACE}`);
+
+ console.log(`Space: ${space.name} (${space.id})`);
+ console.log(`Default network: ${space.network}`);
+ console.log(`Strategies (${space.strategies.length}):`);
+ for (const [i, s] of space.strategies.entries()) {
+ const addr = (s.params as any)?.address;
+ console.log(` ${i + 1}. ${s.name} (network=${s.network}) address=${addr ? shortAddr(addr) : "n/a"}`);
+ }
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
+
+
+
+
diff --git a/scripts/publish-snapshot-settings.ts b/scripts/publish-snapshot-settings.ts
new file mode 100644
index 0000000..e30dfa7
--- /dev/null
+++ b/scripts/publish-snapshot-settings.ts
@@ -0,0 +1,128 @@
+/**
+ * Publish a signed Snapshot Space settings message to Snapshot Hub.
+ *
+ * This is the missing step when using Safe "Messages": collecting signatures in Safe does NOT
+ * automatically broadcast the signed payload to Snapshot Hub.
+ *
+ * Usage:
+ * pnpm exec tsx scripts/publish-snapshot-settings.ts --file ./space-message.json --sig 0x...
+ *
+ * Optional:
+ * --hub https://hub.snapshot.org
+ * --space quickvote.eth
+ *
+ * The JSON file must contain the EIP-712 typed data:
+ * { domain, primaryType, types, message }
+ *
+ * Notes:
+ * - Do NOT re-serialize `message.settings` yourself. It must match exactly what was signed.
+ * - For Safe, `--sig` should be the Safe "prepared signature" (concatenated signatures).
+ */
+
+type TypedData = {
+ domain: { name: string; version: string };
+ primaryType: string;
+ types: Record>;
+ message: { from: string; space: string; timestamp: string | number; settings: string };
+};
+
+import { getAddress } from "viem";
+
+function getArg(flag: string): string | undefined {
+ const i = process.argv.indexOf(flag);
+ if (i === -1) return undefined;
+ return process.argv[i + 1];
+}
+
+async function postJson(url: string, body: unknown) {
+ const res = await fetch(url, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify(body),
+ });
+
+ const text = await res.text().catch(() => "");
+ let json: any = undefined;
+ try {
+ json = text ? JSON.parse(text) : undefined;
+ } catch {
+ // ignore
+ }
+
+ if (!res.ok) {
+ throw new Error(`Hub request failed (${res.status}): ${text.slice(0, 500)}`);
+ }
+
+ return json ?? text;
+}
+
+async function main() {
+ const file = getArg("--file") || process.env.SNAPSHOT_TYPED_DATA_FILE;
+ const sig = getArg("--sig") || process.env.SNAPSHOT_SIG;
+ const hub = (getArg("--hub") || process.env.SNAPSHOT_HUB || "https://hub.snapshot.org").replace(/\/+$/, "");
+ const overrideSpace = getArg("--space") || process.env.SNAPSHOT_SPACE;
+
+ if (!file) {
+ throw new Error("Missing --file (or SNAPSHOT_TYPED_DATA_FILE).");
+ }
+ if (!sig || !sig.startsWith("0x")) {
+ throw new Error("Missing --sig (or SNAPSHOT_SIG). Expected 0x-prefixed hex string.");
+ }
+
+ const fs = await import("node:fs");
+ const raw = fs.readFileSync(file, "utf8");
+ const typed = JSON.parse(raw) as TypedData;
+
+ // Normalize without changing semantics:
+ // - timestamp must be a number for some encoders; keep value identical.
+ const timestamp =
+ typeof typed.message.timestamp === "string" ? Number(typed.message.timestamp) : typed.message.timestamp;
+ if (!Number.isFinite(timestamp)) throw new Error("Invalid message.timestamp");
+
+ const message = {
+ ...typed.message,
+ timestamp,
+ space: overrideSpace ?? typed.message.space,
+ };
+
+ // Snapshot Hub expects EIP-55 checksummed addresses in the envelope.
+ // Also normalize message.from to checksum to avoid mismatches.
+ const from = getAddress(message.from);
+ message.from = from;
+
+ const data = {
+ domain: typed.domain,
+ types: typed.types,
+ primaryType: typed.primaryType,
+ message,
+ };
+
+ // Snapshot Hub expects:
+ // { address, sig, data }
+ // Where `address` is the signer (Safe address for EIP-1271 verification).
+ const payload = {
+ address: from,
+ sig,
+ data,
+ };
+
+ console.log("Publishing to Snapshot Hub…");
+ console.log(" hub:", hub);
+ console.log(" space:", message.space);
+ console.log(" from:", message.from);
+ console.log(" api:", `${hub}/api/msg`);
+
+ const result = await postJson(`${hub}/api/msg`, payload);
+ console.log("\n✅ Hub response:");
+ console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
+
+ console.log("\nNext: verify with:");
+ console.log(" pnpm exec tsx scripts/check-snapshot-space.ts");
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
+
+