[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6FXJVT877F8C9R7DERXJNPB":3,"skill-same:01M6FXJVT877F8C9R7DERXJNPB":64},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":46,"repositoryHtmlUrl":47,"repositoryHomepageUrl":6,"ownerHtmlUrl":48,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":49,"install":50,"resources":63},"01M6FXJVT877F8C9R7DERXJNPB","jb-terminal-wrapper",null,"Terminal wrapper pattern for extending JBMultiTerminal functionality. Use when: (1) need dynamic\nsplits at pay time, (2) revnet can't modify ruleset data hooks, (3) want atomic pay + distribute\noperations, (4) need to intercept\u002Fredirect tokens before delivery, (5) implementing pay-time\nconfiguration, (6) cash out + bridge\u002Fswap in one tx, (7) cash out + stake redeemed funds.\nCovers IJBTerminal implementation, _acceptFunds pattern from JBSwapTerminalRegistry, beneficiary\nmanipulation for both pay and cash out flows, and the critical mental model that wrappers are\nadditive (not restrictive).","kbarbel640-del\u002Fskills","kbarbel640-del","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F255315490?v=4","01M6CJPPQGCCESXMG3K7BBGQ5J","01M6CJPPX2PB2BCZERFVM2C2B3",1,"MIT","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fmejango\u002Fjuicy\u002Fjb-terminal-wrapper\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"jb terminal wrapper","Wrapper","terminal","time","pay","atomic","flows","critical","distribute","tokens","swap","pattern","delivery","implementation","hooks","use","operations","bridge","model","data","2026-11-10 10:12:30",false,"---\nname: jb-terminal-wrapper\ndescription: |\n  Terminal wrapper pattern for extending JBMultiTerminal functionality. Use when: (1) need dynamic\n  splits at pay time, (2) revnet can't modify ruleset data hooks, (3) want atomic pay + distribute\n  operations, (4) need to intercept\u002Fredirect tokens before delivery, (5) implementing pay-time\n  configuration, (6) cash out + bridge\u002Fswap in one tx, (7) cash out + stake redeemed funds.\n  Covers IJBTerminal implementation, _acceptFunds pattern from JBSwapTerminalRegistry, beneficiary\n  manipulation for both pay and cash out flows, and the critical mental model that wrappers are\n  additive (not restrictive).\n---\n\n# Terminal Wrapper Pattern\n\n## Problem\n\nRevnets and other projects often need extended payment functionality that can't be achieved through\nruleset data hooks alone. Common needs include:\n- Dynamic splits specified at payment time\n- Token interception and redirection\n- Atomic multi-step operations (pay + distribute)\n- Client-specific features without breaking permissionless access\n\n## Context \u002F Trigger Conditions\n\nApply this pattern when:\n- Building payment flows that need dynamic configuration\n- Working with revnets where ruleset hooks can't be edited\n- Need to bundle multiple operations atomically\n- Want to intercept tokens for further processing\n- Implementing \"pay and do X\" flows\n\n## Solution\n\n### Core Architecture\n\nCreate a custom `IJBTerminal` that wraps `JBMultiTerminal`. Use a shared `_acceptFunds` helper\n(pattern from JBSwapTerminalRegistry) to handle ETH\u002FERC20 consistently:\n\n```solidity\ncontract PayWithSplitsTerminal is IJBTerminal {\n    using SafeERC20 for IERC20;\n\n    IJBMultiTerminal public immutable MULTI_TERMINAL;\n    IJBController public immutable CONTROLLER;\n\n    constructor(IJBMultiTerminal _multiTerminal, IJBController _controller) {\n        MULTI_TERMINAL = _multiTerminal;\n        CONTROLLER = _controller;\n    }\n\n    function pay(\n        uint256 projectId,\n        address token,\n        uint256 amount,\n        address beneficiary,\n        uint256 minReturnedTokens,\n        string calldata memo,\n        bytes calldata metadata\n    ) external payable returns (uint256 beneficiaryTokenCount) {\n        \u002F\u002F 1. Parse custom metadata\n        (JBSplit[] memory splits, bytes memory innerMetadata) = _parseMetadata(metadata);\n\n        \u002F\u002F 2. Configure splits if provided\n        if (splits.length > 0) {\n            _configureSplits(projectId, splits);\n        }\n\n        \u002F\u002F 3. Accept funds (handles ETH\u002FERC20 uniformly)\n        uint256 valueToSend = _acceptFunds(token, amount, address(MULTI_TERMINAL));\n\n        \u002F\u002F 4. Forward to underlying terminal\n        beneficiaryTokenCount = MULTI_TERMINAL.pay{value: valueToSend}(\n            projectId,\n            token,\n            amount,\n            beneficiary,\n            minReturnedTokens,\n            memo,\n            innerMetadata\n        );\n\n        \u002F\u002F 5. Distribute reserved tokens\n        CONTROLLER.sendReservedTokensToSplitsOf(projectId);\n\n        return beneficiaryTokenCount;\n    }\n\n    \u002F\u002F\u002F @notice Accept funds from caller and prepare for forwarding.\n    \u002F\u002F\u002F @dev Pattern from JBSwapTerminalRegistry - consolidates token handling.\n    function _acceptFunds(\n        address token,\n        uint256 amount,\n        address spender\n    ) internal returns (uint256 valueToSend) {\n        if (token == JBConstants.NATIVE_TOKEN) {\n            return msg.value; \u002F\u002F Forward ETH\n        }\n\n        \u002F\u002F ERC20: pull from sender, approve spender\n        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);\n        IERC20(token).forceApprove(spender, amount);\n        return 0; \u002F\u002F No ETH to forward\n    }\n}\n```\n\n### Beneficiary Manipulation Pattern\n\nIntercept tokens by setting beneficiary to the wrapper itself:\n\n```solidity\nfunction payAndStake(\n    uint256 projectId,\n    address token,\n    uint256 amount,\n    uint256 minReturnedTokens,\n    bytes calldata metadata\n) external payable returns (uint256 tokenCount) {\n    \u002F\u002F Parse user's desired destination from metadata\n    (address finalDestination, bytes memory stakingParams) = abi.decode(\n        metadata,\n        (address, bytes)\n    );\n\n    \u002F\u002F Receive tokens to this contract\n    tokenCount = MULTI_TERMINAL.pay{value: msg.value}(\n        projectId,\n        token,\n        amount,\n        address(this),  \u002F\u002F \u003C-- Wrapper receives tokens\n        minReturnedTokens,\n        \"\",\n        \"\"\n    );\n\n    \u002F\u002F Do something with the tokens\n    IERC20 projectToken = IERC20(CONTROLLER.TOKENS().tokenOf(projectId));\n\n    \u002F\u002F Example: stake them somewhere on behalf of user\n    _stakeTokens(projectToken, tokenCount, finalDestination, stakingParams);\n\n    return tokenCount;\n}\n```\n\n### Metadata Encoding (Client Side)\n\n```typescript\nimport { encodeAbiParameters, parseAbiParameters } from 'viem';\n\n\u002F\u002F For dynamic splits\nconst metadata = encodeAbiParameters(\n  parseAbiParameters('(address preferredBeneficiary, uint256 percent, uint256 lockedUntil)[], bytes'),\n  [\n    [\n      { preferredBeneficiary: '0x...', percent: 500000000n, lockedUntil: 0n }, \u002F\u002F 50%\n      { preferredBeneficiary: '0x...', percent: 500000000n, lockedUntil: 0n }, \u002F\u002F 50%\n    ],\n    '0x' \u002F\u002F Inner metadata for MultiTerminal\n  ]\n);\n\n\u002F\u002F For beneficiary redirection\nconst metadata = encodeAbiParameters(\n  parseAbiParameters('address finalDestination, bytes stakingParams'),\n  [userAddress, stakingCalldata]\n);\n```\n\n### Critical Mental Model\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│                    WRAPPER IS ADDITIVE                          │\n├─────────────────────────────────────────────────────────────────┤\n│                                                                 │\n│   Client A ──► PayWithSplitsTerminal ──► JBMultiTerminal       │\n│                    (gets special features)                      │\n│                                                                 │\n│   Client B ────────────────────────────► JBMultiTerminal       │\n│                    (still works!)                               │\n│                                                                 │\n│   BOTH ARE VALID. The wrapper cannot block direct access.       │\n│   This is a FEATURE, not a bug. Permissionless = good.          │\n│                                                                 │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n**Bad thinking**: \"I'll use a wrapper to block payments that don't meet criteria X\"\n**Reality**: Users can always call `JBMultiTerminal.pay()` directly\n\n**Good thinking**: \"I'll use a wrapper to provide enhanced functionality for clients that opt in\"\n\n### Use Cases\n\n| Use Case | How Wrapper Helps |\n|----------|-------------------|\n| **Pay Wrappers** | |\n| Dynamic splits at pay time | Parse splits from metadata, configure before pay |\n| Pay + distribute reserved | Atomic operation, no separate tx needed |\n| Token interception | Receive to self, then stake\u002Flock\u002Fforward |\n| Referral tracking | Parse referrer from metadata, record on-chain |\n| Conditional logic | Check conditions before forwarding to MultiTerminal |\n| Multi-hop payments | Receive tokens, swap, pay another project |\n| **Cash Out Wrappers** | |\n| Cash out + bridge | Intercept redeemed funds, bridge to another chain |\n| Cash out + swap | Swap redeemed ETH to stablecoin before delivery |\n| Cash out + stake | Stake redeemed funds in another protocol |\n| Cash out + LP | Add redeemed funds to liquidity pool |\n\n### Cash Out Wrapper Pattern\n\nSame beneficiary-to-self trick works for cash outs:\n\n```solidity\n\u002F\u002F\u002F @notice Cash out with automatic swap to different token.\nfunction cashOutAndSwap(\n    address holder,\n    uint256 projectId,\n    uint256 tokenCount,\n    address tokenToReclaim,\n    uint256 minTokensReclaimed,\n    address tokenOut,       \u002F\u002F Custom param: swap to this\n    uint256 minAmountOut,   \u002F\u002F Custom param: slippage\n    address beneficiary,\n    bytes calldata metadata\n) external returns (uint256 amountOut) {\n    \u002F\u002F 1. Cash out to THIS contract (intercept funds)\n    uint256 reclaimAmount = MULTI_TERMINAL.cashOutTokensOf(\n        holder,\n        projectId,\n        tokenCount,\n        tokenToReclaim,\n        minTokensReclaimed,\n        address(this),  \u002F\u002F \u003C-- Wrapper receives redeemed funds\n        metadata\n    );\n\n    \u002F\u002F 2. Swap redeemed tokens to desired output\n    amountOut = _swap(tokenToReclaim, tokenOut, reclaimAmount, minAmountOut);\n\n    \u002F\u002F 3. Send swapped tokens to beneficiary\n    _sendFunds(tokenOut, amountOut, beneficiary);\n\n    return amountOut;\n}\n\n\u002F\u002F\u002F @notice Cash out with automatic bridging.\nfunction cashOutAndBridge(\n    address holder,\n    uint256 projectId,\n    uint256 tokenCount,\n    address tokenToReclaim,\n    uint256 minTokensReclaimed,\n    address beneficiary,\n    uint256 destChainId,\n    bytes calldata metadata\n) external returns (uint256 reclaimAmount) {\n    \u002F\u002F 1. Cash out to this contract\n    reclaimAmount = MULTI_TERMINAL.cashOutTokensOf(\n        holder,\n        projectId,\n        tokenCount,\n        tokenToReclaim,\n        minTokensReclaimed,\n        address(this),\n        metadata\n    );\n\n    \u002F\u002F 2. Bridge funds to destination chain\n    _bridgeFunds(tokenToReclaim, reclaimAmount, beneficiary, destChainId);\n\n    return reclaimAmount;\n}\n```\n\n### Comparison with Swap Terminal\n\nSwap Terminal is a canonical example of this pattern:\n\n```\nUser pays with USDC ──► SwapTerminal ──► Swaps to ETH ──► JBMultiTerminal\n                        (wraps + transforms)\n```\n\nYour wrapper follows the same architecture but with different transformation logic.\n\n## Verification\n\n1. Deploy wrapper pointing to existing JBMultiTerminal\n2. Test that direct MultiTerminal payments still work (permissionless)\n3. Test that wrapper payments get enhanced behavior\n4. Verify atomic operations complete or revert together\n5. Test metadata parsing edge cases (empty, malformed)\n\n## Example\n\n**Complete implementation for pay-time splits:**\n\n```solidity\n\u002F\u002F SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport {IJBTerminal} from \"@bananapus\u002Fcore\u002Fsrc\u002Finterfaces\u002FIJBTerminal.sol\";\nimport {IJBMultiTerminal} from \"@bananapus\u002Fcore\u002Fsrc\u002Finterfaces\u002FIJBMultiTerminal.sol\";\nimport {IJBController} from \"@bananapus\u002Fcore\u002Fsrc\u002Finterfaces\u002FIJBController.sol\";\nimport {IJBSplits} from \"@bananapus\u002Fcore\u002Fsrc\u002Finterfaces\u002FIJBSplits.sol\";\nimport {JBSplit} from \"@bananapus\u002Fcore\u002Fsrc\u002Fstructs\u002FJBSplit.sol\";\nimport {JBSplitGroup} from \"@bananapus\u002Fcore\u002Fsrc\u002Fstructs\u002FJBSplitGroup.sol\";\n\ncontract DynamicSplitsTerminal is IJBTerminal {\n    IJBMultiTerminal public immutable MULTI_TERMINAL;\n    IJBController public immutable CONTROLLER;\n\n    \u002F\u002F Split group ID for reserved tokens\n    uint256 constant RESERVED_TOKEN_GROUP = 1;\n\n    error InvalidSplitTotal();\n\n    constructor(IJBMultiTerminal _multiTerminal, IJBController _controller) {\n        MULTI_TERMINAL = _multiTerminal;\n        CONTROLLER = _controller;\n    }\n\n    \u002F\u002F\u002F @notice Pay a project with dynamic splits specified in metadata\n    \u002F\u002F\u002F @param projectId The project to pay\n    \u002F\u002F\u002F @param token The token to pay with (use JBConstants.NATIVE_TOKEN for ETH)\n    \u002F\u002F\u002F @param amount The amount to pay\n    \u002F\u002F\u002F @param beneficiary Who receives the project tokens\n    \u002F\u002F\u002F @param minReturnedTokens Minimum tokens to receive (slippage protection)\n    \u002F\u002F\u002F @param memo Payment memo\n    \u002F\u002F\u002F @param metadata ABI-encoded (JBSplit[], bytes innerMetadata)\n    function pay(\n        uint256 projectId,\n        address token,\n        uint256 amount,\n        address beneficiary,\n        uint256 minReturnedTokens,\n        string calldata memo,\n        bytes calldata metadata\n    ) external payable returns (uint256 beneficiaryTokenCount) {\n        bytes memory innerMetadata;\n\n        \u002F\u002F Parse and apply splits if metadata provided\n        if (metadata.length > 0) {\n            JBSplit[] memory splits;\n            (splits, innerMetadata) = abi.decode(metadata, (JBSplit[], bytes));\n\n            if (splits.length > 0) {\n                _validateAndSetSplits(projectId, splits);\n            }\n        }\n\n        \u002F\u002F Forward payment to MultiTerminal\n        beneficiaryTokenCount = MULTI_TERMINAL.pay{value: msg.value}(\n            projectId,\n            token,\n            amount,\n            beneficiary,\n            minReturnedTokens,\n            memo,\n            innerMetadata\n        );\n\n        \u002F\u002F Distribute reserved tokens to the new splits\n        CONTROLLER.sendReservedTokensToSplitsOf(projectId);\n\n        return beneficiaryTokenCount;\n    }\n\n    function _validateAndSetSplits(uint256 projectId, JBSplit[] memory splits) internal {\n        \u002F\u002F Validate splits sum to 100% (1e9 = JBConstants.SPLITS_TOTAL_PERCENT)\n        uint256 total;\n        for (uint256 i; i \u003C splits.length; i++) {\n            total += splits[i].percent;\n        }\n        if (total != 1e9) revert InvalidSplitTotal();\n\n        \u002F\u002F Get current ruleset\n        uint256 rulesetId = CONTROLLER.currentRulesetOf(projectId).id;\n\n        \u002F\u002F Set splits for reserved token group\n        JBSplitGroup[] memory groups = new JBSplitGroup[](1);\n        groups[0] = JBSplitGroup({\n            groupId: RESERVED_TOKEN_GROUP,\n            splits: splits\n        });\n\n        CONTROLLER.setSplitGroupsOf(projectId, rulesetId, groups);\n    }\n\n    \u002F\u002F Implement other IJBTerminal functions as pass-through...\n    function addToBalanceOf(\n        uint256 projectId,\n        address token,\n        uint256 amount,\n        bool shouldReturnHeldFees,\n        string calldata memo,\n        bytes calldata metadata\n    ) external payable {\n        MULTI_TERMINAL.addToBalanceOf{value: msg.value}(\n            projectId, token, amount, shouldReturnHeldFees, memo, metadata\n        );\n    }\n}\n```\n\n## Notes\n\n- Wrapper must be granted appropriate permissions if setting splits (add to project's permission system)\n- Consider gas costs of extra operations\n- Metadata parsing adds attack surface - validate carefully\n- For revnets: this is often the ONLY way to add functionality post-deploy\n- Multiple wrappers can exist for different purposes - they don't conflict\n- Wrappers can be chained: WrapperA → WrapperB → MultiTerminal\n\n## Related Skills\n\n- `\u002Fjb-patterns` - All JB V5 design patterns (includes condensed version of this)\n- `\u002Fjb-pay-hook` - Data hooks for pay-time logic (when ruleset allows)\n- `\u002Fjb-split-hook` - Custom split distribution logic\n- `\u002Fjb-v5-api` - Core terminal and controller interfaces\n","# Terminal Wrapper Pattern\n\n## Problem\n\nRevnets and other projects often need extended payment functionality that can't be achieved through\nruleset data hooks alone. Common needs include:\n- Dynamic splits specified at payment time\n- Token interception and redirection\n- Atomic multi-step operations (pay + distribute)\n- Client-specific features without breaking permissionless access\n\n## Context \u002F Trigger Conditions\n\nApply this pattern when:\n- Building payment flows that need dynamic configuration\n- Working with revnets where ruleset hooks can't be edited\n- Need to bundle multiple operations atomically\n- Want to intercept tokens for further processing\n- Implementing \"pay and do X\" flows\n\n## Solution\n\n### Core Architecture\n\nCreate a custom `IJBTerminal` that wraps `JBMultiTerminal`. Use a shared `_acceptFunds` helper\n(pattern from JBSwapTerminalRegistry) to handle ETH\u002FERC20 consistently:\n\n```solidity\ncontract PayWithSplitsTerminal is IJBTerminal {\n    using SafeERC20 for IERC20;\n\n    IJBMultiTerminal public immutable MULTI_TERMINAL;\n    IJBController public immutable CONTROLLER;\n\n    constructor(IJBMultiTerminal _multiTerminal, IJBController _controller) {\n        MULTI_TERMINAL = _multiTerminal;\n        CONTROLLER = _controller;\n    }\n\n    function pay(\n        uint256 projectId,\n        address token,\n        uint256 amount,\n        address beneficiary,\n        uint256 minReturnedTokens,\n        string calldata memo,\n        bytes calldata metadata\n    ) external payable returns (uint256 beneficiaryTokenCount) {\n        \u002F\u002F 1. Parse custom metadata\n        (JBSplit[] memory splits, bytes memory innerMetadata) = _parseMetadata(metadata);\n\n        \u002F\u002F 2. Configure splits if provided\n        if (splits.length > 0) {\n            _configureSplits(projectId, splits);\n        }\n\n        \u002F\u002F 3. Accept funds (handles ETH\u002FERC20 uniformly)\n        uint256 valueToSend = _acceptFunds(token, amount, address(MULTI_TERMINAL));\n\n        \u002F\u002F 4. Forward to underlying terminal\n        beneficiaryTokenCount = MULTI_TERMINAL.pay{value: valueToSend}(\n            projectId,\n            token,\n            amount,\n            beneficiary,\n            minReturnedTokens,\n            memo,\n            innerMetadata\n        );\n\n        \u002F\u002F 5. Distribute reserved tokens\n        CONTROLLER.sendReservedTokensToSplitsOf(projectId);\n\n        return beneficiaryTokenCount;\n    }\n\n    \u002F\u002F\u002F @notice Accept funds from caller and prepare for forwarding.\n    \u002F\u002F\u002F @dev Pattern from JBSwapTerminalRegistry - consolidates token handling.\n    function _acceptFunds(\n        address token,\n        uint256 amount,\n        address spender\n    ) internal returns (uint256 valueToSend) {\n        if (token == JBConstants.NATIVE_TOKEN) {\n            return msg.value; \u002F\u002F Forward ETH\n        }\n\n        \u002F\u002F ERC20: pull from sender, approve spender\n        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);\n        IERC20(token).forceApprove(spender, amount);\n        return 0; \u002F\u002F No ETH to forward\n    }\n}\n```\n\n### Beneficiary Manipulation Pattern\n\nIntercept tokens by setting beneficiary to the wrapper itself:\n\n```solidity\nfunction payAndStake(\n    uint256 projectId,\n    address token,\n    uint256 amount,\n    uint256 minReturnedTokens,\n    bytes calldata metadata\n) external payable returns (uint256 tokenCount) {\n    \u002F\u002F Parse user's desired destination from metadata\n    (address finalDestination, bytes memory stakingParams) = abi.decode(\n        metadata,\n        (address, bytes)\n    );\n\n    \u002F\u002F Receive tokens to this contract\n    tokenCount = MULTI_TERMINAL.pay{value: msg.value}(\n        projectId,\n        token,\n        amount,\n        address(this),  \u002F\u002F \u003C-- Wrapper receives tokens\n        minReturnedTokens,\n        \"\",\n        \"\"\n    );\n\n    \u002F\u002F Do something with the tokens\n    IERC20 projectToken = IERC20(CONTROLLER.TOKENS().tokenOf(projectId));\n\n    \u002F\u002F Example: stake them somewhere on behalf of user\n    _stakeTokens(projectToken, tokenCount, finalDestination, stakingParams);\n\n    return tokenCount;\n}\n```\n\n### Metadata Encoding (Client Side)\n\n```typescript\nimport { encodeAbiParameters, parseAbiParameters } from 'viem';\n\n\u002F\u002F For dynamic splits\nconst metadata = encodeAbiParameters(\n  parseAbiParameters('(address preferredBeneficiary, uint256 percent, uint256 lockedUntil)[], bytes'),\n  [\n    [\n      { preferredBeneficiary: '0x...', percent: 500000000n, lockedUntil: 0n }, \u002F\u002F 50%\n      { preferredBeneficiary: '0x...', percent: 500000000n, lockedUntil: 0n }, \u002F\u002F 50%\n    ],\n    '0x' \u002F\u002F Inner metadata for MultiTerminal\n  ]\n);\n\n\u002F\u002F For beneficiary redirection\nconst metadata = encodeAbiParameters(\n  parseAbiParameters('address finalDestination, bytes stakingParams'),\n  [userAddress, stakingCalldata]\n);\n```\n\n### Critical Mental Model\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│                    WRAPPER IS ADDITIVE                          │\n├─────────────────────────────────────────────────────────────────┤\n│                                                                 │\n│   Client A ──► PayWithSplitsTerminal ──► JBMultiTerminal       │\n│                    (gets special features)                      │\n│                                                                 │\n│   Client B ────────────────────────────► JBMultiTerminal       │\n│                    (still works!)                               │\n│                                                                 │\n│   BOTH ARE VALID. The wrapper cannot block direct access.       │\n│   This is a FEATURE, not a bug. Permissionless = good.          │\n│                                                                 │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n**Bad thinking**: \"I'll use a wrapper to block payments that don't meet criteria X\"\n**Reality**: Users can always call `JBMultiTerminal.pay()` directly\n\n**Good thinking**: \"I'll use a wrapper to provide enhanced functionality for clients that opt in\"\n\n### Use Cases\n\n| Use Case | How Wrapper Helps |\n|----------|-------------------|\n| **Pay Wrappers** | |\n| Dynamic splits at pay time | Parse splits from metadata, configure before pay |\n| Pay + distribute reserved | Atomic operation, no separate tx needed |\n| Token interception | Receive to self, then stake\u002Flock\u002Fforward |\n| Referral tracking | Parse referrer from metadata, record on-chain |\n| Conditional logic | Check conditions before forwarding to MultiTerminal |\n| Multi-hop payments | Receive tokens, swap, pay another project |\n| **Cash Out Wrappers** | |\n| Cash out + bridge | Intercept redeemed funds, bridge to another chain |\n| Cash out + swap | Swap redeemed ETH to stablecoin before delivery |\n| Cash out + stake | Stake redeemed funds in another protocol |\n| Cash out + LP | Add redeemed funds to liquidity pool |\n\n### Cash Out Wrapper Pattern\n\nSame beneficiary-to-self trick works for cash outs:\n\n```solidity\n\u002F\u002F\u002F @notice Cash out with automatic swap to different token.\nfunction cashOutAndSwap(\n    address holder,\n    uint256 projectId,\n    uint256 tokenCount,\n    address tokenToReclaim,\n    uint256 minTokensReclaimed,\n    address tokenOut,       \u002F\u002F Custom param: swap to this\n    uint256 minAmountOut,   \u002F\u002F Custom param: slippage\n    address beneficiary,\n    bytes calldata metadata\n) external returns (uint256 amountOut) {\n    \u002F\u002F 1. Cash out to THIS contract (intercept funds)\n    uint256 reclaimAmount = MULTI_TERMINAL.cashOutTokensOf(\n        holder,\n        projectId,\n        tokenCount,\n        tokenToReclaim,\n        minTokensReclaimed,\n        address(this),  \u002F\u002F \u003C-- Wrapper receives redeemed funds\n        metadata\n    );\n\n    \u002F\u002F 2. Swap redeemed tokens to desired output\n    amountOut = _swap(tokenToReclaim, tokenOut, reclaimAmount, minAmountOut);\n\n    \u002F\u002F 3. Send swapped tokens to beneficiary\n    _sendFunds(tokenOut, amountOut, beneficiary);\n\n    return amountOut;\n}\n\n\u002F\u002F\u002F @notice Cash out with automatic bridging.\nfunction cashOutAndBridge(\n    address holder,\n    uint256 projectId,\n    uint256 tokenCount,\n    address tokenToReclaim,\n    uint256 minTokensReclaimed,\n    address beneficiary,\n    uint256 destChainId,\n    bytes calldata metadata\n) external returns (uint256 reclaimAmount) {\n    \u002F\u002F 1. Cash out to this contract\n    reclaimAmount = MULTI_TERMINAL.cashOutTokensOf(\n        holder,\n        projectId,\n        tokenCount,\n        tokenToReclaim,\n        minTokensReclaimed,\n        address(this),\n        metadata\n    );\n\n    \u002F\u002F 2. Bridge funds to destination chain\n    _bridgeFunds(tokenToReclaim, reclaimAmount, beneficiary, destChainId);\n\n    return reclaimAmount;\n}\n```\n\n### Comparison with Swap Terminal\n\nSwap Terminal is a canonical example of this pattern:\n\n```\nUser pays with USDC ──► SwapTerminal ──► Swaps to ETH ──► JBMultiTerminal\n                        (wraps + transforms)\n```\n\nYour wrapper follows the same architecture but with different transformation logic.\n\n## Verification\n\n1. Deploy wrapper pointing to existing JBMultiTerminal\n2. Test that direct MultiTerminal payments still work (permissionless)\n3. Test that wrapper payments get enhanced behavior\n4. Verify atomic operations complete or revert together\n5. Test metadata parsing edge cases (empty, malformed)\n\n## Example\n\n**Complete implementation for pay-time splits:**\n\n```solidity\n\u002F\u002F SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport {IJBTerminal} from \"@bananapus\u002Fcore\u002Fsrc\u002Finterfaces\u002FIJBTerminal.sol\";\nimport {IJBMultiTerminal} from \"@bananapus\u002Fcore\u002Fsrc\u002Finterfaces\u002FIJBMultiTerminal.sol\";\nimport {IJBController} from \"@bananapus\u002Fcore\u002Fsrc\u002Finterfaces\u002FIJBController.sol\";\nimport {IJBSplits} from \"@bananapus\u002Fcore\u002Fsrc\u002Finterfaces\u002FIJBSplits.sol\";\nimport {JBSplit} from \"@bananapus\u002Fcore\u002Fsrc\u002Fstructs\u002FJBSplit.sol\";\nimport {JBSplitGroup} from \"@bananapus\u002Fcore\u002Fsrc\u002Fstructs\u002FJBSplitGroup.sol\";\n\ncontract DynamicSplitsTerminal is IJBTerminal {\n    IJBMultiTerminal public immutable MULTI_TERMINAL;\n    IJBController public immutable CONTROLLER;\n\n    \u002F\u002F Split group ID for reserved tokens\n    uint256 constant RESERVED_TOKEN_GROUP = 1;\n\n    error InvalidSplitTotal();\n\n    constructor(IJBMultiTerminal _multiTerminal, IJBController _controller) {\n        MULTI_TERMINAL = _multiTerminal;\n        CONTROLLER = _controller;\n    }\n\n    \u002F\u002F\u002F @notice Pay a project with dynamic splits specified in metadata\n    \u002F\u002F\u002F @param projectId The project to pay\n    \u002F\u002F\u002F @param token The token to pay with (use JBConstants.NATIVE_TOKEN for ETH)\n    \u002F\u002F\u002F @param amount The amount to pay\n    \u002F\u002F\u002F @param beneficiary Who receives the project tokens\n    \u002F\u002F\u002F @param minReturnedTokens Minimum tokens to receive (slippage protection)\n    \u002F\u002F\u002F @param memo Payment memo\n    \u002F\u002F\u002F @param metadata ABI-encoded (JBSplit[], bytes innerMetadata)\n    function pay(\n        uint256 projectId,\n        address token,\n        uint256 amount,\n        address beneficiary,\n        uint256 minReturnedTokens,\n        string calldata memo,\n        bytes calldata metadata\n    ) external payable returns (uint256 beneficiaryTokenCount) {\n        bytes memory innerMetadata;\n\n        \u002F\u002F Parse and apply splits if metadata provided\n        if (metadata.length > 0) {\n            JBSplit[] memory splits;\n            (splits, innerMetadata) = abi.decode(metadata, (JBSplit[], bytes));\n\n            if (splits.length > 0) {\n                _validateAndSetSplits(projectId, splits);\n            }\n        }\n\n        \u002F\u002F Forward payment to MultiTerminal\n        beneficiaryTokenCount = MULTI_TERMINAL.pay{value: msg.value}(\n            projectId,\n            token,\n            amount,\n            beneficiary,\n            minReturnedTokens,\n            memo,\n            innerMetadata\n        );\n\n        \u002F\u002F Distribute reserved tokens to the new splits\n        CONTROLLER.sendReservedTokensToSplitsOf(projectId);\n\n        return beneficiaryTokenCount;\n    }\n\n    function _validateAndSetSplits(uint256 projectId, JBSplit[] memory splits) internal {\n        \u002F\u002F Validate splits sum to 100% (1e9 = JBConstants.SPLITS_TOTAL_PERCENT)\n        uint256 total;\n        for (uint256 i; i \u003C splits.length; i++) {\n            total += splits[i].percent;\n        }\n        if (total != 1e9) revert InvalidSplitTotal();\n\n        \u002F\u002F Get current ruleset\n        uint256 rulesetId = CONTROLLER.currentRulesetOf(projectId).id;\n\n        \u002F\u002F Set splits for reserved token group\n        JBSplitGroup[] memory groups = new JBSplitGroup[](1);\n        groups[0] = JBSplitGroup({\n            groupId: RESERVED_TOKEN_GROUP,\n            splits: splits\n        });\n\n        CONTROLLER.setSplitGroupsOf(projectId, rulesetId, groups);\n    }\n\n    \u002F\u002F Implement other IJBTerminal functions as pass-through...\n    function addToBalanceOf(\n        uint256 projectId,\n        address token,\n        uint256 amount,\n        bool shouldReturnHeldFees,\n        string calldata memo,\n        bytes calldata metadata\n    ) external payable {\n        MULTI_TERMINAL.addToBalanceOf{value: msg.value}(\n            projectId, token, amount, shouldReturnHeldFees, memo, metadata\n        );\n    }\n}\n```\n\n## Notes\n\n- Wrapper must be granted appropriate permissions if setting splits (add to project's permission system)\n- Consider gas costs of extra operations\n- Metadata parsing adds attack surface - validate carefully\n- For revnets: this is often the ONLY way to add functionality post-deploy\n- Multiple wrappers can exist for different purposes - they don't conflict\n- Wrappers can be chained: WrapperA → WrapperB → MultiTerminal\n\n## Related Skills\n\n- `\u002Fjb-patterns` - All JB V5 design patterns (includes condensed version of this)\n- `\u002Fjb-pay-hook` - Data hooks for pay-time logic (when ruleset allows)\n- `\u002Fjb-split-hook` - Custom split distribution logic\n- `\u002Fjb-v5-api` - Core terminal and controller interfaces\n",{"name":5,"description":7},"main","e7e336ac628c3e3bd771aff1aaa469c93778f94a","skills\u002Fmejango\u002Fjuicy\u002Fjb-terminal-wrapper",0,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del","2026-08-23 22:39:37",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add kbarbel640-del\u002Fskills --skill jb-terminal-wrapper",{"tool":56,"command":57},"bunx","bunx skills add kbarbel640-del\u002Fskills --skill jb-terminal-wrapper",{"tool":59,"command":60},"pnpm","pnpm dlx skills add kbarbel640-del\u002Fskills --skill jb-terminal-wrapper","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fmejango\u002Fjuicy\u002Fjb-terminal-wrapper\u002FSKILL.md",true,[],[65,94,122,143,149,169],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":76,"publishedAt":38},"01M6G8J55967D6281ACMKAKTEQ","crowd-prompting","A marketplace where AI agents improve prompts, system instructions, tool descriptions, and other text-based content with domain expertise from real-world operations — and earn tokens for valuable contributions.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzzadrian\u002Fcrowd-prompting\u002FSKILL.md",[71],{"categoryNo":72,"categoryName":73,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[77,78,79,80,27,81,82,83,34,84,85,86,87,88,89,90,91,92,93],"crowd prompting","prompting","api","with","domain","world","prompts","instructions","text","tool","system","improve","productivity","marketplace","https","content","ai agents",{"skillNo":95,"name":96,"nameZh":6,"description":97,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":98,"categories":99,"keywords":105,"publishedAt":38},"01M6G8J0PKPAYVET6BWE8W4C26","dilemm-ai","Compete in DilemmAI, the prisoner's dilemma AI arena at dilemm.ai. Use when an OpenClaw agent wants to create an account, design and submit strategy prompts for their bot, enter matchmaking, analyze historical matches and leaderboards, and iterate on bot strategy. Requires headless browser automation (no API available yet).","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzyori\u002Fdilemm-ai\u002FSKILL.md",[100],{"categoryNo":101,"categoryName":102,"taxonomyType":103,"taxonomyTypeDesc":104},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[106,79,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,83,33],"dilemm ai","arena","analyze","browser","openclaw agent","create","automation","browser automation","bot","openclaw","design","strategy","headless","compete","submit","matchmaking",{"skillNo":123,"name":124,"nameZh":6,"description":125,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":126,"categories":127,"keywords":131,"publishedAt":38},"01M6G8HWAF3K9SW7NP3Z83CPJ6","arxiv-paper-reviews","Interact with arXiv Crawler API to fetch papers, read reviews, and submit comments. Use when working with arXiv papers, fetching paper lists by date\u002Fcategory\u002Finterest, viewing paper details with comments, or submitting paper reviews via API at http:\u002F\u002F150.158.152.82:8000.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Fweak-accept\u002FSKILL.md",[128],{"categoryNo":129,"categoryName":130,"taxonomyType":103,"taxonomyTypeDesc":104},"01JBSEEDCAT000000000000021","资料检索与研究",[132,133,134,135,136,137,138,80,139,120,33,140,79,141,142],"arxiv paper reviews","reviews","arxiv","paper","crawler","papers","comments","fetch","read","http","via",{"skillNo":144,"name":124,"nameZh":6,"description":125,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":148,"publishedAt":38},"01M6G8HQJ7N7WMY6YV7D7SSJ16","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Farxiv-paper-reviews\u002FSKILL.md",[147],{"categoryNo":129,"categoryName":130,"taxonomyType":103,"taxonomyTypeDesc":104},[135,134,133,132,142,137,138,80,136,120,33,139,140,79,141],{"skillNo":150,"name":151,"nameZh":6,"description":152,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":153,"categories":154,"keywords":155,"publishedAt":38},"01M6G8HM95Y64JNVFC2HN4E2BG","git-summary","Get a quick summary of the current Git repository including status, recent commits, branches, and contributors.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzweack\u002Fgit-summary\u002FSKILL.md",[],[156,157,158,159,160,161,162,163,164,165,166,167,168],"git","summary","git summary","commits","repository","win32","status","linux","contributors","true","quick","get","branches",{"skillNo":170,"name":171,"nameZh":6,"description":172,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":173,"categories":174,"keywords":179,"publishedAt":38},"01M6G8HGEGF05CNEJCGVYFSC4R","agent-constitution","Interact with AgentConstitution governance contracts on Base Sepolia. Check compliance, read rules, log actions, query governance state.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fztsalexey\u002Fagent-constitution\u002FSKILL.md",[175,176],{"categoryNo":101,"categoryName":102,"taxonomyType":103,"taxonomyTypeDesc":104},{"categoryNo":177,"categoryName":178,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000037","律师与法务",[180,181,182,183,80,184,185,43,186,140,187,188,189,190,191,91,192,193,194],"agent constitution","constitution","contracts","tree","query","base","log","check","Rules","projects","compliance","actions","state","governance","github"]