skills/develop-postparam-hooks/SKILL.md
Design, write, test, deploy, and register custom Art Blocks PostParam hook contracts in Solidity. Use when an artist or developer wants to build a custom IPMPAugmentHook (inject live on-chain data into tokenData) or IPMPConfigureHook (validate/gate collector param changes), or both combined. Covers hook type selection, Solidity templates, lightweight testing in Foundry or Hardhat, deployment to Sepolia and mainnet, and registration via the Creator Dashboard.
npx skillsauth add artblocks/skills develop-postparam-hooksInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
PostParam hooks are Solidity contracts that extend Art Blocks' PostParams system. Two hook types exist:
| Hook type | Interface | Triggered by | Can revert? | Use for |
|---|---|---|---|---|
| Augment | IPMPAugmentHook | Every getTokenParams() read | No (view) | Inject live on-chain data into tokenData |
| Configure | IPMPConfigureHook | Every configureTokenParams() write | Yes | Validate or gate collector param changes |
| Combined | Both | Both | Augment: no; Configure: yes | Full control (e.g. LiftHooks) |
Build an augment hook if you want to:
Build a configure hook if you want to:
Build a combined hook (implement both interfaces in one contract) when you need both.
Check reference.md for the full list of standard augment hooks — you may not need to write one at all.
The interfaces are small and self-contained (only depend on OpenZeppelin IERC165).
Foundry — copy the three interface files into src/interfaces/:
IPMPAugmentHook.solIPMPConfigureHook.solIWeb3Call.solInstall OpenZeppelin as your only dependency:
forge install OpenZeppelin/openzeppelin-contracts
Hardhat / npm — install the published package for interfaces + ABIs:
npm install @artblocks/contracts @openzeppelin/contracts
See the ready-to-use templates in templates/. Abbreviated versions below.
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.22;
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
import {IWeb3Call} from "./interfaces/IWeb3Call.sol";
import {IPMPAugmentHook} from "./interfaces/IPMPAugmentHook.sol";
contract MyAugmentHook is IPMPAugmentHook {
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(IPMPAugmentHook).interfaceId
|| interfaceId == type(IERC165).interfaceId;
}
function onTokenPMPReadAugmentation(
address coreContract,
uint256 tokenId,
IWeb3Call.TokenParam[] calldata tokenParams
) external view override returns (IWeb3Call.TokenParam[] memory augmentedTokenParams) {
uint256 n = tokenParams.length;
augmentedTokenParams = new IWeb3Call.TokenParam[](n + 1);
for (uint256 i = 0; i < n; i++) {
augmentedTokenParams[i] = tokenParams[i]; // must copy all existing params
}
augmentedTokenParams[n] = IWeb3Call.TokenParam({
key: "myKey",
value: _computeValue(coreContract, tokenId)
});
}
function _computeValue(address coreContract, uint256 tokenId)
internal view returns (string memory)
{
// TODO: your on-chain read logic here
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.22;
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
import {IPMPConfigureHook} from "./interfaces/IPMPConfigureHook.sol";
import {IPMPV0} from "./interfaces/IPMPV0.sol";
contract MyConfigureHook is IPMPConfigureHook {
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(IPMPConfigureHook).interfaceId
|| interfaceId == type(IERC165).interfaceId;
}
function onTokenPMPConfigure(
address coreContract,
uint256 tokenId,
IPMPV0.PMPInput calldata pmpInput
) external override {
// Revert to block invalid configuration. Reverts here roll back
// the collector's entire configureTokenParams transaction.
require(/* your validation logic */, "Invalid configuration");
}
}
Implement both onTokenPMPReadAugmentation and onTokenPMPConfigure in one
contract and return true for both interface IDs in supportsInterface. See
templates/CombinedHook.sol for the full pattern.
No fork or full repo clone required for unit tests.
Unit test (Foundry) — call the hook directly:
function test_augmentation_appends_key() public {
MyAugmentHook hook = new MyAugmentHook();
IWeb3Call.TokenParam[] memory input = new IWeb3Call.TokenParam[](1);
input[0] = IWeb3Call.TokenParam({key: "existingKey", value: "existingValue"});
IWeb3Call.TokenParam[] memory result =
hook.onTokenPMPReadAugmentation(address(0), 0, input);
assertEq(result.length, 2);
assertEq(result[0].key, "existingKey"); // original param preserved
assertEq(result[1].key, "myKey"); // new param appended
}
function test_supportsInterface() public {
MyAugmentHook hook = new MyAugmentHook();
assertTrue(hook.supportsInterface(type(IPMPAugmentHook).interfaceId));
assertTrue(hook.supportsInterface(type(IERC165).interfaceId));
}
Integration test against real PMPV0 — fork Sepolia:
forge test --fork-url $SEPOLIA_RPC_URL
// Uses live PMPV0 at 0x00000000A78E278b2d2e2935FaeBe19ee9F1FF14
IPMPV0 pmpV0 = IPMPV0(0x00000000A78E278b2d2e2935FaeBe19ee9F1FF14);
IWeb3Call.TokenParam[] memory params = pmpV0.getTokenParams(coreContract, tokenId);
// assert your hook's injected key appears in params
Deploy to Sepolia staging first, then repeat for mainnet.
# Foundry
forge create src/MyAugmentHook.sol:MyAugmentHook \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $PRIVATE_KEY
# Verify on Etherscan
forge verify-contract <deployed-address> src/MyAugmentHook.sol:MyAugmentHook \
--chain sepolia --etherscan-api-key $ETHERSCAN_API_KEY
# Hardhat
npx hardhat run scripts/deploy.js --network sepolia
npx hardhat verify <deployed-address> --network sepolia
The Creator Dashboard is the recommended way to attach your hook:
To register programmatically instead, call PMPV0 directly (artist wallet required):
IPMPV0(0x00000000A78E278b2d2e2935FaeBe19ee9F1FF14).configureProjectHooks(
coreContract,
projectId,
IPMPConfigureHook(address(0)), // pass address(0) if not using configure hook
IPMPAugmentHook(myAugmentHook) // pass address(0) if not using augment hook
);
PMPV0 is deployed at the same address on all chains (mainnet, Sepolia, Arbitrum, Base).
const postParams = tokenData.externalAssetDependencies[0];
const myValue = postParams?.data?.["myKey"]; // string or undefined
Verify the correct dependency index in Creator Dashboard under Scripts → Flex Assets.
tokenData.supportsInterface is validated on-chain at registration time — if it returns false for the correct interface ID, configureProjectHooks reverts.view — no state changes, no events.getTokenParams(). Keep computation cheap; avoid unbounded loops or heavy storage reads.tools
Build or convert Art Blocks generative art scripts using artblocks-mcp. Use when helping a user create, scaffold, port, or convert an art script for Art Blocks, or when working with tokenData, hash-based PRNG, FLEX dependencies, PostParams, window.$features traits, p5.js, Three.js, or the Art Blocks generator format.
tools
Query Art Blocks on-chain data using the artblocks-mcp GraphQL tools. Use when fetching projects, tokens, artists, sales, traits, or any Art Blocks on-chain data via graphql_query, build_query, explore_table, graphql_introspection, validate_fields, or query_optimizer. These are advanced escape-hatch tools — prefer domain-specific tools (discover_projects, get_project, get_artist, get_wallet_tokens, get_token_metadata) when they cover the use case.
tools
Mint (purchase) an Art Blocks token using the artblocks-mcp tools. Use when a user wants to mint, purchase, or buy an Art Blocks NFT, or needs to understand minting mechanics, minter types, pricing, allowlists, Dutch auctions, or build_purchase_transaction.
tools
Retrieve rich metadata for a specific Art Blocks token using artblocks-mcp. Use when a user wants to look up a minted token's details, traits, features, media URLs, owner, listing info, live view, or project context using get_token_metadata.