Skip to content

Commit f086cfc

Browse files
committed
The listing: same graph, Manual trigger, $0.05 on the Marketplace
`sync.mjs --trigger manual` emits gavel-drain-<chain>-listed.json — the identical graph with the trigger swapped from Webhook to Manual, which is what a Marketplace listing takes caller input through (P7). The webhook graph is byte-identical before and after this change. Listed 2026-09-11T12:36:25Z as slug `gavel-drain`, workflow iv621084hfcq326rszuzb, workflowType write (settled BEFORE listing — #2227 freezes it at the publish PATCH), $0.05 USDC per call. POST https://app.keeperhub.com/api/mcp/workflows/gavel-drain/call answers 402 with an x402 v2 requirement on eip155:8453 and the input schema in the bazaar extension. Because the row is `write`, a paid call returns execTransaction calldata the caller signs and sends themselves — a judge can drain a roster Safe from their own wallet.
1 parent 7023d2e commit f086cfc

2 files changed

Lines changed: 276 additions & 8 deletions

File tree

‎scripts/sync.mjs‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* node scripts/sync.mjs --chain 8453
2020
* node scripts/sync.mjs --chain 11155111 --safe-integration <id> [--discord-integration <id>]
2121
* node scripts/sync.mjs --chain 11155111 --safe-integration <id> --push
22+
* node scripts/sync.mjs --chain 11155111 --safe-integration <id> --trigger manual # Marketplace graph
2223
*/
2324

2425
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
@@ -46,6 +47,15 @@ if (!chain) {
4647
process.exit(1);
4748
}
4849

50+
/**
51+
* Trigger type. Webhook is the default and what the sweeper calls. A Marketplace
52+
* listing takes caller input through a Manual trigger (P7; docs/workflows/marketplace),
53+
* so `--trigger manual` emits the same graph with the trigger swapped and the
54+
* five template references renamed. Nothing else changes.
55+
*/
56+
const TRIGGER = String(flags.trigger ?? 'webhook').toLowerCase() === 'manual' ? 'Manual' : 'Webhook';
57+
const SAFE_ADDR = `{{@trigger-1:${TRIGGER}.safeAddress}}`;
58+
4959
/** The Safe ABI fragments we need. execTransaction is the only manual ABI in the project. */
5060
const EXEC_ABI = [{
5161
inputs: [
@@ -110,7 +120,7 @@ const ROSTER = ${roster};
110120
// their own assemble() declaration, which hoists over the real one and
111121
// bypasses every guard at once. gate-addr below rejects non-addresses before
112122
// this node runs; this line makes the splice non-syntactic regardless.
113-
const __safeAddress = {{@trigger-1:Webhook.safeAddress}};
123+
const __safeAddress = ${SAFE_ADDR};
114124
const __hydrated = {{@hydrate-1:Hydrate Signatures.data}};
115125
const __transactions = (__hydrated && __hydrated.results) ? __hydrated.results : [];
116126
@@ -141,7 +151,6 @@ return assemble({
141151
});`;
142152
}
143153

144-
const SAFE_ADDR = '{{@trigger-1:Webhook.safeAddress}}';
145154
const node = (id, label, type, config, x, y) => ({
146155
id, type: type === 'trigger' ? 'trigger' : 'action',
147156
position: { x, y },
@@ -186,7 +195,7 @@ if (!web3Integration || web3Integration === true) {
186195
}
187196

188197
const nodes = [
189-
node('trigger-1', 'Webhook', 'trigger', { triggerType: 'Webhook' }, 0, 0),
198+
node('trigger-1', TRIGGER, 'trigger', { triggerType: TRIGGER }, 0, 0),
190199

191200
// The first thing that touches caller input. A webhook (or the public
192201
// Marketplace listing) can send anything; nothing downstream should have to
@@ -202,7 +211,7 @@ const nodes = [
202211
// `malformed-payload` for anything that slips past this.
203212
node('gate-addr', 'Valid Address', 'action', {
204213
actionType: 'Condition',
205-
condition: '{{@trigger-1:Webhook.safeAddress}}.startsWith("0x") && {{@trigger-1:Webhook.safeAddress}}.length === 42',
214+
condition: `${SAFE_ADDR}.startsWith("0x") && ${SAFE_ADDR}.length === 42`,
206215
}, 250, 0),
207216

208217
node('queue-1', 'Safe Queue', 'action', {
@@ -276,11 +285,11 @@ if (discord) {
276285
nodes.push(
277286
node('report-exec-1', 'Report Executed', 'action', {
278287
actionType: 'discord/send-message', integrationId: discord,
279-
message: 'gavel · {{@trigger-1:Webhook.safeAddress}} · nonce {{@assemble-1:Assemble.result.nonce}}\nEXECUTED → {{@exec-1:Exec Transaction.transactionHash}}',
288+
message: `gavel · ${SAFE_ADDR} · nonce {{@assemble-1:Assemble.result.nonce}}\nEXECUTED → {{@exec-1:Exec Transaction.transactionHash}}`,
280289
}, 2500, -100),
281290
node('report-skip-1', 'Report Skipped', 'action', {
282291
actionType: 'discord/send-message', integrationId: discord,
283-
message: 'gavel · {{@trigger-1:Webhook.safeAddress}} · nonce {{@assemble-1:Assemble.result.nonce}}\nNOT executed → {{@assemble-1:Assemble.result.reason}}\n{{@assemble-1:Assemble.result.detail}}',
292+
message: `gavel · ${SAFE_ADDR} · nonce {{@assemble-1:Assemble.result.nonce}}\nNOT executed → {{@assemble-1:Assemble.result.reason}}\n{{@assemble-1:Assemble.result.detail}}`,
284293
}, 2250, 100),
285294
);
286295
edges.push(
@@ -290,14 +299,14 @@ if (discord) {
290299
}
291300

292301
const workflow = {
293-
name: `gavel-drain-${chainId}`,
302+
name: `gavel-drain-${chainId}${TRIGGER === 'Manual' ? '-listed' : ''}`,
294303
description: 'Executes Safe transactions that already reached their signature threshold but were never executed. Generated by scripts/sync.mjs — do not hand-edit in the canvas.',
295304
nodes, edges,
296305
};
297306
if (MANIFEST.projectId) workflow.projectId = MANIFEST.projectId;
298307

299308
mkdirSync(join(ROOT, 'workflows'), { recursive: true });
300-
const out = join(ROOT, 'workflows', `gavel-drain-${chainId}.json`);
309+
const out = join(ROOT, 'workflows', `gavel-drain-${chainId}${TRIGGER === 'Manual' ? '-listed' : ''}.json`);
301310
writeFileSync(out, JSON.stringify(workflow, null, 2));
302311

303312
console.log(`\n ${chain.name} (${chainId}) — ${nodes.length} nodes, ${edges.length} edges`);

0 commit comments

Comments
 (0)