skills$openclaw/jb-hook-deploy-ui
mejango2.1k

by mejango

jb-hook-deploy-ui – OpenClaw Skill

jb-hook-deploy-ui is an OpenClaw Skills integration for coding workflows. Deploy custom Juicebox hooks from browser. Compile Solidity, deploy contracts, verify on explorers, and attach to projects. Works with Claude-generated hooks.

2.1k stars8.9k forksSecurity L1
Updated Feb 7, 2026Created Feb 7, 2026coding

Skill Snapshot

namejb-hook-deploy-ui
descriptionDeploy custom Juicebox hooks from browser. Compile Solidity, deploy contracts, verify on explorers, and attach to projects. Works with Claude-generated hooks. OpenClaw Skills integration.
ownermejango
repositorymejango/juicypath: jb-hook-deploy-ui
languageMarkdown
licenseMIT
topics
securityL1
installopenclaw add @mejango/juicy:jb-hook-deploy-ui
last updatedFeb 7, 2026

Maintainer

mejango

mejango

Maintains jb-hook-deploy-ui in the OpenClaw Skills directory.

View GitHub profile
File Explorer
1 files
jb-hook-deploy-ui
SKILL.md
25.2 KB
SKILL.md

name: jb-hook-deploy-ui description: Deploy custom Juicebox hooks from browser. Compile Solidity, deploy contracts, verify on explorers, and attach to projects. Works with Claude-generated hooks.

Juicebox V5 Hook Deployment UI

Deploy custom hooks directly from the browser. Compile Solidity code, deploy to any chain, verify on block explorers, and attach to your Juicebox project.

Overview

1. Paste or load Solidity source code
2. Compile in browser using solc.js
3. Deploy via connected wallet
4. Optionally verify on Etherscan/Basescan/etc.
5. Attach hook to project ruleset

Complete Hook Deploy UI Template

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Deploy Custom Hook</title>
  <link rel="stylesheet" href="/shared/styles.css">
  <style>
    .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
    @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
    .step { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 0; border-bottom: 1px solid var(--border-color); }
    .step:last-child { border-bottom: none; }
    .step-number { width: 28px; height: 28px; border-radius: 50%; background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; font-size: 0.875rem; font-weight: 600; flex-shrink: 0; }
    .step-number.active { background: var(--accent); }
    .step-number.complete { background: var(--success); }
    .step-title { font-weight: 500; }
    .step-desc { font-size: 0.75rem; color: var(--text-muted); }
    .constructor-args { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border-color); }
    .arg-input { margin-bottom: 0.5rem; }
    .arg-input label { font-size: 0.75rem; }
    .chain-select { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
    .chain-chip { padding: 0.5rem 1rem; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; font-size: 0.875rem; background: var(--bg-primary); color: var(--text-primary); }
    .chain-chip.selected { background: var(--accent); border-color: var(--accent); }
    .output { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 0.75rem; font-family: monospace; font-size: 0.8rem; overflow-x: auto; white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow-y: auto; }
  </style>
</head>
<body>
  <div class="container">
    <h1>Deploy Custom Hook</h1>
    <p style="color: var(--text-muted); margin-bottom: 1.5rem;">Compile, deploy, and attach hooks to your Juicebox project</p>

    <div class="card" style="margin-bottom: 1rem;">
      <button class="btn" id="connect-btn" onclick="connectWallet()">Connect Wallet</button>
      <div id="wallet-info" class="hidden" style="margin-top: 0.75rem;">
        <span class="badge-success">Connected: <span id="wallet-address"></span> on <span id="wallet-chain"></span></span>
      </div>
    </div>

    <div class="card" style="margin-bottom: 1rem;">
      <div class="step"><div class="step-number" id="step1-num">1</div><div><div class="step-title">Source Code</div><div class="step-desc">Paste or load Solidity code</div></div></div>
      <div class="step"><div class="step-number" id="step2-num">2</div><div><div class="step-title">Compile</div><div class="step-desc">Generate ABI and bytecode</div></div></div>
      <div class="step"><div class="step-number" id="step3-num">3</div><div><div class="step-title">Deploy</div><div class="step-desc">Deploy contract to chain</div></div></div>
      <div class="step"><div class="step-number" id="step4-num">4</div><div><div class="step-title">Attach</div><div class="step-desc">Set as project hook</div></div></div>
    </div>

    <div class="grid">
      <div>
        <div class="card">
          <h2>Source Code</h2>
          <div class="tabs" style="margin-bottom: 1rem;">
            <div class="tab active" onclick="showTab('paste')">Paste</div>
            <div class="tab" onclick="showTab('catalog')">From Catalog</div>
            <div class="tab" onclick="showTab('file')">Upload</div>
          </div>

          <div id="tab-paste">
            <textarea id="source-code" rows="18" style="font-family: monospace; font-size: 0.8rem;" placeholder="// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {IJBPayHook} from '@bananapus/core/src/interfaces/IJBPayHook.sol';
// ... your hook code"></textarea>
          </div>

          <div id="tab-catalog" class="hidden">
            <label>Select Hook Template</label>
            <select id="catalog-select" onchange="loadFromCatalog()">
              <option value="">-- Select a template --</option>
              <optgroup label="Pay Hooks">
                <option value="pay-cap">Payment Cap Hook</option>
                <option value="pay-allowlist">Allowlist Pay Hook</option>
              </optgroup>
              <optgroup label="Cash Out Hooks">
                <option value="cashout-fee">Fee Extraction Hook</option>
              </optgroup>
            </select>
            <div id="catalog-preview" class="output" style="margin-top: 0.5rem; min-height: 100px;"></div>
          </div>

          <div id="tab-file" class="hidden">
            <input type="file" id="file-input" accept=".sol" onchange="loadFromFile()">
          </div>

          <button class="btn" style="margin-top: 1rem;" onclick="compileSource()">Compile</button>
        </div>

        <div class="card">
          <h2>Compiler Output</h2>
          <div id="compile-status"></div>
          <div id="compile-errors" class="output hidden" style="color: var(--error);"></div>

          <div id="compile-success" class="hidden">
            <label>Select Contract</label>
            <select id="contract-select" onchange="selectContract()">
              <option value="">-- Select contract to deploy --</option>
            </select>
            <div id="constructor-args" class="constructor-args hidden">
              <label style="font-weight: 600;">Constructor Arguments</label>
              <div id="args-container"></div>
            </div>
          </div>
        </div>
      </div>

      <div>
        <div class="card">
          <h2>Target Chain</h2>
          <div class="chain-select" id="chain-select">
            <div class="chain-chip selected" data-chain="1" onclick="selectChain(this)">Ethereum</div>
            <div class="chain-chip" data-chain="11155111" onclick="selectChain(this)">Sepolia</div>
            <div class="chain-chip" data-chain="10" onclick="selectChain(this)">Optimism</div>
            <div class="chain-chip" data-chain="8453" onclick="selectChain(this)">Base</div>
            <div class="chain-chip" data-chain="42161" onclick="selectChain(this)">Arbitrum</div>
          </div>
        </div>

        <div class="card">
          <h2>Deploy Contract</h2>
          <div id="deploy-status"></div>
          <button class="btn" id="deploy-btn" onclick="deployContract()" disabled>Deploy Hook</button>

          <div id="deploy-result" class="hidden" style="margin-top: 1rem;">
            <label>Contract Address</label>
            <div class="output" id="deployed-address"></div>
            <div style="display: flex; gap: 0.5rem; margin-top: 0.75rem;">
              <button class="btn-secondary" onclick="copyAddress()">Copy</button>
              <button class="btn-secondary" onclick="viewOnExplorer()">Explorer</button>
            </div>
          </div>
        </div>

        <div class="card">
          <h2>Verify Contract (Optional)</h2>
          <label>Etherscan API Key</label>
          <input type="text" id="etherscan-key" placeholder="Your API key">
          <button class="btn-secondary" id="verify-btn" onclick="verifyContract()" disabled>Verify</button>
          <div id="verify-status" style="margin-top: 0.5rem;"></div>
        </div>

        <div class="card">
          <h2>Attach to Project</h2>
          <label>Project ID</label>
          <input type="number" id="project-id" placeholder="e.g., 1">
          <label>Hook Type</label>
          <select id="hook-type">
            <option value="pay">Pay Hook</option>
            <option value="cashout">Cash Out Hook</option>
            <option value="both">Both</option>
          </select>
          <button class="btn" id="attach-btn" onclick="attachToProject()" disabled>Queue Ruleset with Hook</button>
          <div id="attach-status" style="margin-top: 0.5rem;"></div>
        </div>
      </div>
    </div>
  </div>

  <script type="module">
    import { createWalletClient, custom, encodeDeployData, encodeFunctionData } from 'https://esm.sh/viem';
    import { mainnet, optimism, base, arbitrum, sepolia } from 'https://esm.sh/viem/chains';
    import { CHAIN_CONFIGS, truncateAddress } from '/shared/wallet-utils.js';

    const CHAINS_MAP = { 1: mainnet, 10: optimism, 8453: base, 42161: arbitrum, 11155111: sepolia };
    const EXPLORERS = {
      1: { name: 'Etherscan', url: 'https://etherscan.io', api: 'https://api.etherscan.io' },
      11155111: { name: 'Sepolia', url: 'https://sepolia.etherscan.io', api: 'https://api-sepolia.etherscan.io' },
      10: { name: 'Optimism', url: 'https://optimistic.etherscan.io', api: 'https://api-optimistic.etherscan.io' },
      8453: { name: 'Basescan', url: 'https://basescan.org', api: 'https://api.basescan.org' },
      42161: { name: 'Arbiscan', url: 'https://arbiscan.io', api: 'https://api.arbiscan.io' }
    };

    const HOOK_CATALOG = {
      'pay-cap': { name: 'Payment Cap Hook', source: `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {IJBPayHook} from "@bananapus/core/src/interfaces/IJBPayHook.sol";
import {JBAfterPayRecordedContext} from "@bananapus/core/src/structs/JBAfterPayRecordedContext.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

contract PaymentCapHook is IJBPayHook {
    uint256 public immutable MAX_PAYMENT;
    error PaymentExceedsCap(uint256 amount, uint256 cap);

    constructor(uint256 _maxPayment) { MAX_PAYMENT = _maxPayment; }

    function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable override {
        if (context.amount.value > MAX_PAYMENT) revert PaymentExceedsCap(context.amount.value, MAX_PAYMENT);
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IJBPayHook).interfaceId || interfaceId == type(IERC165).interfaceId;
    }
}` },
      'pay-allowlist': { name: 'Allowlist Pay Hook', source: `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {IJBPayHook} from "@bananapus/core/src/interfaces/IJBPayHook.sol";
import {JBAfterPayRecordedContext} from "@bananapus/core/src/structs/JBAfterPayRecordedContext.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract AllowlistPayHook is IJBPayHook, Ownable {
    mapping(address => bool) public allowed;
    error NotAllowed(address payer);

    constructor(address[] memory _allowed) Ownable(msg.sender) {
        for (uint256 i; i < _allowed.length; i++) allowed[_allowed[i]] = true;
    }

    function setAllowed(address addr, bool status) external onlyOwner { allowed[addr] = status; }

    function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable override {
        if (!allowed[context.payer]) revert NotAllowed(context.payer);
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IJBPayHook).interfaceId || interfaceId == type(IERC165).interfaceId;
    }
}` },
      'cashout-fee': { name: 'Fee Extraction Hook', source: `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {IJBCashOutHook} from "@bananapus/core/src/interfaces/IJBCashOutHook.sol";
import {JBAfterCashOutRecordedContext} from "@bananapus/core/src/structs/JBAfterCashOutRecordedContext.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

contract FeeExtractionHook is IJBCashOutHook {
    address payable public immutable FEE_RECIPIENT;
    uint256 public constant FEE_BPS = 500; // 5%

    constructor(address payable _feeRecipient) { FEE_RECIPIENT = _feeRecipient; }

    function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context) external payable override {
        uint256 fee = (context.forwardedAmount.value * FEE_BPS) / 10000;
        if (fee > 0) FEE_RECIPIENT.transfer(fee);
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IJBCashOutHook).interfaceId || interfaceId == type(IERC165).interfaceId;
    }
}` }
    };

    let walletClient = null;
    let connectedAddress = null;
    let compiledContracts = {};
    let selectedContract = null;
    let deployedAddress = null;
    let selectedChainId = 1;
    let sourceCode = '';

    window.showTab = function(tab) {
      document.querySelectorAll('.tabs .tab').forEach(t => t.classList.remove('active'));
      event.target.classList.add('active');
      document.getElementById('tab-paste').classList.toggle('hidden', tab !== 'paste');
      document.getElementById('tab-catalog').classList.toggle('hidden', tab !== 'catalog');
      document.getElementById('tab-file').classList.toggle('hidden', tab !== 'file');
    };

    window.loadFromCatalog = function() {
      const template = HOOK_CATALOG[document.getElementById('catalog-select').value];
      if (template) {
        document.getElementById('catalog-preview').textContent = template.source;
        document.getElementById('source-code').value = template.source;
        sourceCode = template.source;
      }
    };

    window.loadFromFile = function() {
      const file = document.getElementById('file-input').files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = (e) => {
        document.getElementById('source-code').value = e.target.result;
        sourceCode = e.target.result;
      };
      reader.readAsText(file);
    };

    window.selectChain = function(el) {
      document.querySelectorAll('.chain-chip').forEach(c => c.classList.remove('selected'));
      el.classList.add('selected');
      selectedChainId = parseInt(el.dataset.chain);
    };

    window.connectWallet = async function() {
      if (!window.ethereum) { alert('Please install a web3 wallet'); return; }
      try {
        const [address] = await window.ethereum.request({ method: 'eth_requestAccounts' });
        connectedAddress = address;
        walletClient = createWalletClient({ chain: CHAINS_MAP[selectedChainId], transport: custom(window.ethereum) });

        const chainId = await window.ethereum.request({ method: 'eth_chainId' });
        document.getElementById('wallet-address').textContent = truncateAddress(address);
        document.getElementById('wallet-chain').textContent = CHAINS_MAP[parseInt(chainId, 16)]?.name || `Chain ${parseInt(chainId, 16)}`;
        document.getElementById('wallet-info').classList.remove('hidden');
        document.getElementById('connect-btn').textContent = 'Connected';
        document.getElementById('connect-btn').disabled = true;
        updateStep(1, 'active');
      } catch (e) { console.error(e); alert('Failed to connect'); }
    };

    window.compileSource = async function() {
      sourceCode = document.getElementById('source-code').value;
      if (!sourceCode.trim()) { showStatus('compile-status', 'error', 'Please enter source code'); return; }

      showStatus('compile-status', 'info', 'Compiling... (requires compilation API)');
      document.getElementById('compile-errors').classList.add('hidden');
      document.getElementById('compile-success').classList.add('hidden');

      try {
        const response = await fetch('/api/compile', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ source: sourceCode })
        });

        if (!response.ok) throw new Error('Compilation service unavailable');
        const result = await response.json();

        if (result.errors?.some(e => e.severity === 'error')) {
          const errors = result.errors.filter(e => e.severity === 'error');
          document.getElementById('compile-errors').textContent = errors.map(e => e.formattedMessage).join('\n');
          document.getElementById('compile-errors').classList.remove('hidden');
          showStatus('compile-status', 'error', `Compilation failed with ${errors.length} error(s)`);
          return;
        }

        compiledContracts = {};
        for (const [fileName, file] of Object.entries(result.contracts || {})) {
          for (const [contractName, contract] of Object.entries(file)) {
            compiledContracts[contractName] = { abi: contract.abi, bytecode: contract.evm.bytecode.object };
          }
        }

        if (Object.keys(compiledContracts).length === 0) {
          showStatus('compile-status', 'error', 'No contracts found');
          return;
        }

        const select = document.getElementById('contract-select');
        select.innerHTML = '<option value="">-- Select contract --</option>' +
          Object.keys(compiledContracts).map(n => `<option value="${n}">${n}</option>`).join('');
        document.getElementById('compile-success').classList.remove('hidden');
        showStatus('compile-status', 'success', `Compiled ${Object.keys(compiledContracts).length} contract(s)`);
        updateStep(2, 'complete');
      } catch (error) {
        showStatus('compile-status', 'error', `Error: ${error.message}`);
      }
    };

    window.selectContract = function() {
      const name = document.getElementById('contract-select').value;
      if (!name) { selectedContract = null; document.getElementById('deploy-btn').disabled = true; return; }

      selectedContract = compiledContracts[name];
      const constructor = selectedContract.abi.find(i => i.type === 'constructor');
      const argsContainer = document.getElementById('args-container');
      argsContainer.innerHTML = '';

      if (constructor?.inputs?.length > 0) {
        document.getElementById('constructor-args').classList.remove('hidden');
        constructor.inputs.forEach((input, i) => {
          argsContainer.innerHTML += `<div class="arg-input"><label>${input.name} (${input.type})</label><input type="text" id="arg-${i}" placeholder="${input.type}"></div>`;
        });
      } else {
        document.getElementById('constructor-args').classList.add('hidden');
      }
      document.getElementById('deploy-btn').disabled = false;
    };

    window.deployContract = async function() {
      if (!selectedContract || !connectedAddress) { showStatus('deploy-status', 'error', 'Select contract first'); return; }

      const chainId = await window.ethereum.request({ method: 'eth_chainId' });
      if (parseInt(chainId, 16) !== selectedChainId) {
        showStatus('deploy-status', 'warning', `Please switch to ${CHAINS_MAP[selectedChainId]?.name}`);
        try {
          await window.ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x' + selectedChainId.toString(16) }] });
          walletClient = createWalletClient({ chain: CHAINS_MAP[selectedChainId], transport: custom(window.ethereum) });
        } catch (e) { return; }
      }

      showStatus('deploy-status', 'info', 'Deploying...');
      document.getElementById('deploy-btn').disabled = true;

      try {
        const constructor = selectedContract.abi.find(i => i.type === 'constructor');
        const args = [];
        if (constructor?.inputs) {
          constructor.inputs.forEach((input, i) => {
            const value = document.getElementById(`arg-${i}`).value;
            args.push(parseArg(value, input.type));
          });
        }

        const hash = await walletClient.deployContract({
          abi: selectedContract.abi,
          bytecode: `0x${selectedContract.bytecode}`,
          args,
          account: connectedAddress
        });

        showStatus('deploy-status', 'info', 'Waiting for confirmation...');

        // Poll for receipt
        const receipt = await pollForReceipt(hash);
        deployedAddress = receipt.contractAddress;

        document.getElementById('deployed-address').textContent = deployedAddress;
        document.getElementById('deploy-result').classList.remove('hidden');
        showStatus('deploy-status', 'success', 'Deployed!');
        updateStep(3, 'complete');
        document.getElementById('verify-btn').disabled = false;
        document.getElementById('attach-btn').disabled = false;
      } catch (error) {
        showStatus('deploy-status', 'error', `Failed: ${error.message}`);
        document.getElementById('deploy-btn').disabled = false;
      }
    };

    async function pollForReceipt(hash, attempts = 30) {
      for (let i = 0; i < attempts; i++) {
        const receipt = await window.ethereum.request({ method: 'eth_getTransactionReceipt', params: [hash] });
        if (receipt) return receipt;
        await new Promise(r => setTimeout(r, 2000));
      }
      throw new Error('Transaction not confirmed');
    }

    function parseArg(value, type) {
      if (type.startsWith('uint') || type.startsWith('int')) return BigInt(value);
      if (type === 'bool') return value.toLowerCase() === 'true';
      if (type.endsWith('[]')) return JSON.parse(value);
      return value;
    }

    window.copyAddress = () => { navigator.clipboard.writeText(deployedAddress); showStatus('deploy-status', 'success', 'Copied!'); };
    window.viewOnExplorer = () => { window.open(`${EXPLORERS[selectedChainId].url}/address/${deployedAddress}`, '_blank'); };

    window.verifyContract = async function() {
      const apiKey = document.getElementById('etherscan-key').value;
      if (!apiKey) { showStatus('verify-status', 'warning', 'API key required'); return; }

      showStatus('verify-status', 'info', 'Submitting verification...');

      try {
        const response = await fetch(`${EXPLORERS[selectedChainId].api}/api`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: new URLSearchParams({
            apikey: apiKey, module: 'contract', action: 'verifysourcecode',
            contractaddress: deployedAddress, sourceCode: sourceCode,
            codeformat: 'solidity-single-file',
            contractname: document.getElementById('contract-select').value,
            compilerversion: 'v0.8.23+commit.f704f362', optimizationUsed: '1', runs: '200'
          })
        });
        const result = await response.json();
        showStatus('verify-status', result.status === '1' ? 'success' : 'error',
          result.status === '1' ? 'Verification submitted!' : `Failed: ${result.result}`);
      } catch (error) { showStatus('verify-status', 'error', `Error: ${error.message}`); }
    };

    window.attachToProject = function() {
      const projectId = document.getElementById('project-id').value;
      const hookType = document.getElementById('hook-type').value;
      if (!projectId) { showStatus('attach-status', 'error', 'Enter project ID'); return; }

      showStatus('attach-status', 'info', `To attach hook to project ${projectId}:

Call JBController.queueRulesetsOf() with:
- metadata.useDataHookForPay: ${hookType === 'pay' || hookType === 'both'}
- metadata.useDataHookForCashOut: ${hookType === 'cashout' || hookType === 'both'}
- metadata.dataHook: ${deployedAddress}

Use /jb-interact-ui for full ruleset configuration.`);
      updateStep(4, 'complete');
    };

    function showStatus(id, type, msg) {
      const el = document.getElementById(id);
      el.className = type === 'error' ? 'badge-error' : type === 'success' ? 'badge-success' : type === 'warning' ? 'badge-warning' : '';
      el.style.display = 'block';
      el.style.padding = '0.75rem';
      el.style.marginBottom = '0.75rem';
      el.style.whiteSpace = 'pre-wrap';
      el.textContent = msg;
    }

    function updateStep(step, status) {
      document.getElementById(`step${step}-num`).className = 'step-number ' + status;
    }
  </script>
</body>
</html>

Compilation API Endpoint

For browser compilation, you'll need a server-side endpoint:

// pages/api/compile.ts
import solc from 'solc';

export default async function handler(req, res) {
  if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' });

  const { source } = req.body;
  const input = {
    language: 'Solidity',
    sources: { 'contract.sol': { content: source } },
    settings: { outputSelection: { '*': { '*': ['abi', 'evm.bytecode'] } }, optimizer: { enabled: true, runs: 200 } }
  };

  const output = JSON.parse(solc.compile(JSON.stringify(input), { import: findImports }));
  res.json(output);
}

function findImports(path) {
  // Resolve from GitHub/npm
  return { error: 'Import not found: ' + path };
}
const IMPORT_MAPPINGS = {
  '@bananapus/core/': 'https://raw.githubusercontent.com/Bananapus/nana-core-v5/main/',
  '@bananapus/721-hook/': 'https://raw.githubusercontent.com/Bananapus/nana-721-hook-v5/main/',
  '@openzeppelin/contracts/': 'https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/v5.0.0/contracts/'
};
  • /jb-pay-hook - Generate pay hook Solidity code
  • /jb-cash-out-hook - Generate cash out hook Solidity code
  • /jb-split-hook - Generate split hook Solidity code
  • /jb-interact-ui - Queue rulesets with hooks
README.md

No README available.

Permissions & Security

Security level L1: Low-risk skills with minimal permissions. Review inputs and outputs before running in production.

Requirements

  • OpenClaw CLI installed and configured.
  • Language: Markdown
  • License: MIT
  • Topics:

FAQ

How do I install jb-hook-deploy-ui?

Run openclaw add @mejango/juicy:jb-hook-deploy-ui in your terminal. This installs jb-hook-deploy-ui into your OpenClaw Skills catalog.

Does this skill run locally or in the cloud?

OpenClaw Skills execute locally by default. Review the SKILL.md and permissions before running any skill.

Where can I verify the source code?

The source repository is available at https://github.com/openclaw/skills/tree/main/skills/mejango/juicy. Review commits and README documentation before installing.