npx skills add ...
npx skills add decentraland/sdk-skills --skill nft-blockchain
NFT display and blockchain interaction in Decentraland. Use when the user wants NFTs, blockchain, wallet, smart contracts, Web3, or token gating. Do NOT use for player avatar data or emotes (see player-avatar).
npx skills add decentraland/sdk-skills --skill nft-blockchain
Use NftShape to show an NFT in a decorative picture frame. Provide the NFT URN and choose a frame style. The image is loaded automatically from the NFT's metadata (fetched through Decentraland's OpenSea proxy, opensea.decentraland.org) — any NFT that OpenSea supports can be displayed, across multiple chains.
NFT URN format: urn:decentraland:<chain>:<contractStandard>:<contractAddress>:<tokenId>
urn:decentraland:ethereum:erc721:0x06012c8cf97bead5deae237070f9587f8e7a266d:558536<chain> supported values (forwarded verbatim to the OpenSea v2 API; unrecognized chains are rejected): ethereum, matic, klaytn, bsc, arbitrum, arbitrum_nova, avalanche, optimism, solana, base, blast, zora.<contractStandard> is typically erc721. The image is served from the OpenSea response, so contracts OpenSea indexes as erc1155 also resolve.Use getPlayer() from @dcl/sdk/src/players to get the player's Ethereum address via player.userId. Always check isGuest before any blockchain interaction -- guest players don't have a connected wallet.
Use signedFetch from ~system/SignedFetch to send authenticated requests to a backend. It automatically injects signed identity headers (ADR-44) that your backend verifies — you do not build or pass them yourself.
signedFetch({ url, init: { method?, headers?, body? } }).{ ok, status, statusText, headers, body }; body is a string — call JSON.parse(response.body) (there is no .json()).getHeaders({ url, init? }) from the same module — it returns { headers }.For direct smart contract calls, use eth-connect with createEthereumProvider from @dcl/sdk/ethereum-provider. Store ABIs in separate files, create a contract instance via ContractFactory, then call read (no gas) or write (requires gas, prompts user to sign) functions.
Read operations (view/pure functions) don't require gas. Write operations prompt the player to sign and require gas.
Wrap all async blockchain calls in executeTask(async () => { ... }). Handle errors gracefully — blockchain operations can fail (rejected by user, insufficient gas, network issues).
Use requestManager.eth_gasPrice() and requestManager.eth_getBalance() from eth-connect to check current gas prices and account ETH balances.
Use sendAsync from ~system/EthereumController for low-level Ethereum RPC calls not covered by eth-connect helpers.
Use openExternalUrl and openNftDialog from ~system/RestrictedActions to open external links and NFT detail views.
For development, use the Sepolia testnet: set MetaMask to Sepolia, get test ETH from a faucet, deploy contracts to Sepolia. Contract addresses differ between mainnet and testnet.
For common blockchain operations, use dcl-crypto-toolkit instead of raw eth-connect. It provides a cleaner API for the most frequent tasks.
Import: import * as crypto from 'dcl-crypto-toolkit'. Modules: ethereum, mana, currency, nft, marketplace, services, wearable, contract. There is NO top-level crypto.signMessage.
Capabilities:
crypto.mana.send / .myBalance / .balance)crypto.currency.*)crypto.nft.*)executeOrder), sell (createOrder), cancel (cancelOrder), check authorization (crypto.marketplace.*)crypto.ethereum.signMessageAdvanced())By NFT ownership: Use crypto.nft.checkTokens(contractAddress, tokenIds?) — returns whether the player holds tokens of that contract. Omit tokenIds to check any token of the contract. Grant or deny access on the result.
By MANA balance: Check crypto.mana.myBalance() (or crypto.currency.balance(contractAddress, address) for other ERC20 tokens) to gate access based on holdings.
| Task | Use |
|---|---|
| Send MANA | crypto.mana.send() |
| Check own MANA balance | crypto.mana.myBalance() |
| Check any address's MANA balance | crypto.mana.balance(address) |
| Send any ERC20 token | crypto.currency.send() |
| Check ERC20 balance | crypto.currency.balance(contract, address) |
| Transfer an NFT | crypto.nft.transfer() |
| Check NFT ownership / token gating | crypto.nft.checkTokens() |
| Buy from marketplace | crypto.marketplace.executeOrder() |
| List NFT for sale | crypto.marketplace.createOrder() |
| Sign a message | crypto.ethereum.signMessageAdvanced() |
| Custom smart contract | eth-connect (see above) |
| Authenticated API call | signedFetch (see above) |
Engine-team test scenes exercising these APIs against the real runtime:
NftShape.create() with a live urn:decentraland:ethereum:erc721:<contract>:<tokenId> URN, mounted on a moving platform that carries it across the parcel boundary (so it also shows an NFT frame being culled out of bounds).openNftDialog({ urn }) and openExternalUrl driven from React-ECS buttons, alongside every other RestrictedAction. Note the scene declares neither OPEN_EXTERNAL_LINK nor an NFT permission in requiredPermissions and both still run.signedFetch on click; reads response.ok/.status/.body and inspects the auto-added signature headers, with an empty requiredPermissions.For full code examples and implementation patterns, including the dcl-crypto-toolkit library API, see '{baseDir}/references/blockchain-patterns.md'.
npm install eth-connectnpm install dcl-crypto-toolkit