# Implementing ATCP/IP
Source: https://docs.story.foundation/ai-agents/implementing-atcpip
Learn how to practically implement Agent to Agent transactions from the proposed architecture in the ATCP/IP whitepaper.
Still new!
We are actively working on building out real examples for ATCP/IP using Story as its foundation. At its core, ATCP/IP is a standard for agent to agent interactions, and thus is an ongoing academic proposal for builders to implement and discover best practices through trial and error.
Below are details on how to actually implement the ***2. An ATCP/IP Transaction*** section of the whitepaper below.
Read our Agent TCP/IP whitepaper, which defines an agent-to-agent
transaction system to enable a future of AGI.
## Performing Each Step
Below we will show how to implement each step of the ATCP/IP interaction flow as demonstrated by the image below.
### Registering your Agent's Outputs
In order to register an agent's outputs (or really any IP) on Story, follow the [How to Register IP on Story](/developers/tutorials/how-to-register-ip-on-story) tutorial. The only difference is how you structure your IP Metadata, which should always follow the [📝 IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard).
You can also check out more specific tutorials that demonstrate how to register images generated by DALL·E or Stability:
* [Protect DALL·E AI-Generated Images](/developers/tutorials/protect-dalle-ai-generated-images)
* [Register & Monetize Stability Images](/developers/tutorials/register-stability-images)
Here is an example of what the IP Metadata should look like for your generated IP (using a song as an example):
```json theme={null}
{
"title": "Midnight Marriage",
"description": "This is a house-style song generated on suno.",
"createdAt": "1740005219",
"creators": [
{
"name": "Jacob Tucker",
"address": "0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112",
"contributionPercent": 100
}
],
"image": "https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg",
"imageHash": "0xc404730cdcdf7e5e54e8f16bc6687f97c6578a296f4a21b452d8a6ecabd61bcc",
"mediaUrl": "https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3",
"mediaHash": "0xb52a44f53b2485ba772bd4857a443e1fb942cf5dda73c870e2d2238ecd607aee",
"mediaType": "audio/mpeg"
}
```
### Creating Agreement Terms
As described in the [Whitepaper](https://story.foundation/atcpip), agents will negotiate on what agreement terms are appropriate for the requested task:
2 **Terms formulation**: The provider agent will consider the request and choose an appropriate set of\
license terms for the information being requested. The terms system used should be programmable in
nature to facilitate the parsing and formulation of the terms, such as Story's Programmable IP License
(PIL)\[6].
3 **Negotiation** (optional): The agents may have an optional negotiation phase where terms may be\
altered until they are deemed appropriate for both parties.
* **Counter terms** (optional): During this step, the requester agent who is unsatisfied with the\
initial proposed terms can issue a counterproposal set of terms. Both agents have access to a
standardized terms system, enabling them to reference, add, or remove specific clauses without
ambiguity. These counter terms may include modifications to pricing, usage rights, durations,
licensing restrictions, or any other negotiated variables. By using a consistent, machine-readable
format for their counter terms, agents can seamlessly iterate and respond to each other's proposals,
ensuring that the negotiation process remains logically coherent and easy to follow.
* **Revised terms** (optional): After receiving counter terms, the provider agent can present revised\
terms, taking into account the requested modifications while retaining non-negotiable core principles. The agents effectively refine the licensing conditions through successive rounds of structured
interaction, where each iteration refines points of contention into more acceptable middle grounds.
Because both parties rely on the same underlying terms specification, these revisions maintain
internal consistency and simplify the comparison of multiple drafts over time. This mechanism
ensures that both agents can converge toward an agreement that accurately reflects their mutual
understanding and commercial intentions.
* *This process could have multiple iterations until an agreement is reached*
Once agents agree on the terms, they can be created and attached to the registered asset:
Learn how to attach terms to your IP using the SDK.
Learn how to attach terms to your IP using the Smart Contracts.
### Mint a License
As stated in the [Whitepaper](https://story.foundation/atcpip), after agents have negotiated on a set of terms, the requester agent can mint a license from the provider agent with specific agreement terms attached:
4 **Acceptance**: The requester agent will formally accept the terms by minting an immutable token (the\
agreement token) that encapsulates the terms and rules by which the information being provided is
to be used. Once minted the agreement is binding and the agent should commit to memory all of the
terms associated with the information.
* **Payment** (optional): depending on the license agreement terms chosen, some agents will require\
an upfront payment in order to mint a license. Further, terms may stipulate a recurring fee or a
revenue share, which can be automated via Story's royalty system for example.
Once agreement terms are attached to an IP Asset, a [License Token](/concepts/licensing-module/license-token) can be minted:
Learn how to mint a License Token using the SDK.
Learn how to mint a License Token using the Smart Contracts.
Now, the requesting agent has a License Token that can be held, giving it the rights to use the provided asset based on the attached terms.
### Claim Revenue
Once the providing agent has been paid for their work (when the requesting agent minted a license that costed \$), they can claim their due revenue:
Learn how to claim revenue using the SDK.
Learn how to claim revenue using the Smart Contracts.
## Example Integration with MCP
We have implemented a Model Context Protocol (MCP) server that provides tools for interacting with Story's protocol using [the MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk), and an AI Agent that uses those tools.
Run an MCP server locally that has tools for interacting with Story's protocol to test Agent TCP/IP.
A LangGraph-based AI agent for creating, minting, and registering IP assets with Story using the Story MCP server.
1. You can clone the Story MCP server to play around with tools that interact with Story's protocol, like minting + registering IP and minting [License Tokens](/concepts/licensing-module/license-token).
2. Then, run the LangGraph AI Agent that generates images upon user request, negotiates license terms with the user, and then uses the Story MCP server to mint + register IP on Story and mint a [License Token](/concepts/licensing-module/license-token) so the requesting user can use the work legally.
Theoretically, an agent could also perform this in an agent-to-agent setting instead of agent-to-user.
### What is MCP?
> "MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools."
Check out the [Model Context Protocol (MCP) website](https://modelcontextprotocol.io/introduction) to learn more.
# For AI Agents
Source: https://docs.story.foundation/ai-agents/overview
Learn about AI Agents on Story. Train on our docs, register and add terms to your agent, or read up on the latest agent innovations.
This page is all about AI Agents. We have prepared a way for you to use our documentation as training data which can be seen below, or continue to learn about developing AI Agents on Story.
Below you will find two sections:
1. **Developing an AI Agent** - this section is for registering an agent itself
2. **Implementing ATCP/IP** - this section is for implementing the ***2. An ATCP/IP Transaction*** section of the [Whitepaper](https://story.foundation/whitepaper.pdf).
## Developing an Agent
Below are details on how to:
* Register an AI Agent as IP
* Add License Terms to your AI Agent
### Registering an Agent
In order to register an AI Agent (or any IP) on Story, follow the [How to Register IP on Story](/developers/tutorials/how-to-register-ip-on-story) tutorial. The only difference for AI Agents is how you structure your IP Metadata, which should follow the [📝 IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard).
Here is an example of what the IP Metadata should look like for your AI Agent:
```json theme={null}
{
"title": "Story AI Agent",
"description": "This is an example AI Agent registered on Story.",
"createdAt": "1740005219",
"creators": [
{
"name": "Jacob Tucker",
"address": "0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112",
"contributionPercent": 100
}
],
"image": "https://ipfs.io/ipfs/bafybeigi3k77t5h5aefwpzvx3uiomuavdvqwn5rb5uhd7i7xcq466wvute",
"imageHash": "0x64ccc40de203f218d16bb90878ecca4338e566ab329bf7be906493ce77b1551a",
"mediaUrl": "https://ipfs.io/ipfs/bafybeigi3k77t5h5aefwpzvx3uiomuavdvqwn5rb5uhd7i7xcq466wvute",
"mediaHash": "0x64ccc40de203f218d16bb90878ecca4338e566ab329bf7be906493ce77b1551a",
"mediaType": "image/webp",
"aiMetadata": {
"characterFileUrl": "https://ipfs.io/ipfs/bafkreic6eu4hlnwx46soib62rgkhhmlieko67dggu6bzk7bvtfusqsknfu",
"characterFileHash": "0x5e253875b6d7e7a4e407da899473b168229def8cc6a783957c35996928494d2d"
},
"ipType": "AI Agent",
"tags": ["AI Agent", "Twitter bot", "Smart Agent"]
}
```
### Adding Terms to your AI Agent
Upon registering your AI Agent, you can add license terms to it. However you can add more license terms to your AI Agent afterwards as well. Follow the below tutorials to learn how to do this:
Learn how to attach terms to your AI Agent using the SDK.
Learn how to attach terms to your AI Agent using the Smart Contracts.
## Implementing ATCP/IP
See [Implementing the ATCP/IP Whitepaper](/ai-agents/implementing-atcpip).
# Using Cursor with Story
Source: https://docs.story.foundation/ai-agents/using-cursor-with-story
Add the entire Story documentation to Cursor.
[Cursor](https://www.cursor.com/) is an AI code editor that makes it easy to write code while building Story apps. Let's walk through how to setup Cursor for the best possible results with Story.
## Add Story Docs
Adding Story docs lets you interact with our docs directly and get the most accurate answers to your questions.
1. Go to Cursor Settings > Features > Docs and click "+ Add new doc"
2. Paste the URL `https://docs.story.foundation`
This is our entire documentation combined into a single `.md` file, **which is
automatically updated every single time our docs have changes.**
3. Change the name to Story, and leave everything else the same
## Using the Docs
You can then reference the Story docs in your prompt with the `@Story` symbol.
# Access Controller
Source: https://docs.story.foundation/concepts/access-controller
Manages all permission-related states and permission checks on Story.
View the smart contract for the Access Controller.
Access Controller manages all permission-related states and permission checks on Story. In particular, it maintains the *Permission Table* and *Permission Engine* to process and store permissions. IPAccount permissions are set by the IPAccount owner.
## Permission Table
### Permission Record
| IPAccount | Signer (caller) | To (only module) | Function Sig | Permission |
| ---------- | --------------- | ---------------- | ------------ | ---------- |
| 0x123..111 | 0x789..222 | 0x790..333 | 0xAaAaAaAa | Allow |
| 0x123..111 | 0x789..222 | 0x790..333 | 0xBBBBBBBB | Deny |
| 0x123..111 | 0x789..222 | 0x790..333 | 0xCCCCCC | Abstain |
Each record defines a permission in the form of the **Signer** (caller) calling the **Func** of the **To** (module) on behalf of the **IPAccount**.
The permission field can be set as "Allow," "Deny," or "Abstain." Abstain indicates that the permission decision is determined by the upper-level permission.
### Wildcard
Wildcard is also supported when defining permissions; it defines a permission that applies to multiple modules and/or functions.
With wildcards, users can easily define a whitelist or blacklist of permissions.
| IPAccount | Signer (caller) | To (module) | Func | Permission |
| :--------- | :-------------- | :---------- | :--- | :--------- |
| 0x123..111 | 0x789..222 | \* | \* | Allow |
| 0x123..111 | 0x789..222 | 0x790..333 | \* | Deny |
The above example shows that the signer (0x789...) is unable to invoke any functions of the module (0x790...) on behalf of the IPAccount (0x123...).
In other words, the IPAccount has blacklisted the signer from calling any functions on the module 0x790...333
* Supported wildcards:
| Parameter | Wildcard |
| -------------------------- | ---------- |
| Func | bytes4(0) |
| Addresses (IPAccount / To) | address(0) |
### Permission Prioritization
Specific permissions override general permissions.
| IPAccount | Signer (caller) | To (module) | Func | Permission |
| :--------- | :-------------- | :---------- | :--------- | :--------- |
| 0x123..111 | 0x789..222 | \* | \* | Allow |
| 0x123..111 | 0x789..222 | 0x790..333 | \* | Deny |
| 0x123..111 | 0x789..222 | 0x790..333 | 0xCCCCDDDD | Allow |
The above shows that the signer (0x789...) is not allowed to call any functions of the module (0x790...) on behalf of IPAccount (0x123...), except for the function 0xCCCCDDDD
Furthermore, the signer (0x789...) is permitted to call all other modules on behalf of IPAccount (0x123...).
## Call Flows with Access Control
There exist three types of call flows expected by the Access Controller.
1. An IPAccount calls a module directly.
2. A module calls another module directly.
3. A module calls a registry directly.
### IPAccount calling a Module directly
* IPAccount performs a permission check with the Access Controller.
* The module only needs to check if the `msg.sender` is a valid IPAccount.
When calling a module from an IPAccount, the IPAccount performs an access control check with AccessController to determine if the current caller has permission to make the call. In the module, it only needs to check whether the transaction `msg.sender` is a valid IPAccount.
`AccessControlled` provide a modifier `onlyIpAccount()` helps to perform the access control check.
```solidity Solidity theme={null}
contract MockModule is IModule, AccessControlled {
function action(string memory param) external view onlyIpAccount() returns (string memory) {
// do something
}
}
```
## Module calling another Module
* The callee module needs to perform the authorization check itself.
When a module is called directly from another module, it is responsible for performing the access control check using AccessController. This check determines whether the current caller has permission to make the call to the module.
`AccessControlled` provide a modifier `verifyPermission(address ipAccount)` helps to perform the access control check.
```solidity Solidity theme={null}
contract MockModule is IModule, AccessControlled {
function callFromAnotherModule(address ipAccount) external verifyPermission(ipAccount) returns (string memory) {
if (!IAccessController(accessController).checkPermission(ipAccount, msg.sender, address(this), this.callFromAnotherModule.selector)) {
revert Unauthorized();
}
// do something
}
}
```
## Module calling Registry
* The registry performs the authorization check by calling AccessController.
* The registry authorizes modules through set global permission
When a registry is called by a module, it can perform the access control check using AccessController. This check determines whether the callee module has permission to call the registry.
```solidity Solidity theme={null}
// called by StoryProtocl Admin
IAccessController(accessController).setGlobalPermission(address(0), address(module), address(registry), bytes4(0))) {
```
```solidity Solidity theme={null}
contract MockRegistry {
function registerAction() external returns (string memory) {
if (!IAccessController(accessController).checkPermission(address(0), msg.sender, address(this), this.registerAction.selector)) {
revert Unauthorized();
}
// do something
}
}
```
The IPAccount's permissions will be revoked upon transfer of ownership.
The permissions associated with the IPAccount are exclusively linked to its current owner. When the ownership of the IPAccount is transferred to a new individual, the existing permissions granted to the previous owner are automatically revoked. This ensures that only the current, legitimate owner has access to these permissions. If, in the future, the IPAccount ownership is transferred back to the original owner, the permissions that were initially revoked will be reinstated, restoring the original owner's access and control.
# ❌ Dispute Module
Source: https://docs.story.foundation/concepts/dispute-module/overview
A way for users to raise and resolve disputes through arbitration
The Dispute Module creates a way for users to raise and resolve disputes through arbitration.
View the smart contract for the Dispute Module.
## Dispute Terminology
The main components of the arbitration system are:
* **Arbitration Policies:** the arbitration policy refers to the set rules/process/entities that combined will decide on a dispute. Currently the only supported arbitration policy is the [UMA Arbitration Policy](/concepts/dispute-module/uma-arbitration-policy).
* **Arbitration Penalty:** what happens to an IP Asset after it has been "tagged". An IPA is not deemed "tagged" unless the dispute is decided to be correct. Once tagged, an IPA will not be able to:
* mint licenses
* link to any parents
* claim royalties
* and all of its existing licenses become unusable
### Dispute Tags
**Tags** refer to the "labels" that can be applied to IP Assets in the protocol when raising a dispute. **Tags must be whitelisted by protocol governance to be used in a dispute.** The initial set of tags (and their `bytes32` for interacting with the Dispute Module on-chain) are:
* `IMPROPER_REGISTRATION`: `0x494d50524f5045525f524547495354524154494f4e0000000000000000000000`
* `IMPROPER_USAGE`: `0x494d50524f5045525f5553414745000000000000000000000000000000000000`
* `IMPROPER_PAYMENT`: `0x494d50524f5045525f5041594d454e5400000000000000000000000000000000`
* `CONTENT_STANDARDS_VIOLATION`: `0x434f4e54454e545f5354414e44415244535f56494f4c4154494f4e0000000000`
* `IN_DISPUTE`: `0x494e5f4449535055544500000000000000000000000000000000000000000000`
| Dispute Tag | Explanation |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IMPROPER_REGISTRATION` | Refers to registration of IP that already exists. |
| `IMPROPER_USAGE`
Examples (non-exhaustive): Territory, Channels of Distribution, Expiration, Irrevocable, Attribution, Derivatives, Limitations on Creation of Derivatives, Commercial Use, Sublicensable, Non-Transferable, Restriction on Cross-Platform Use | Refers to improper use of an IP Asset across multiple items (examples on the left). These items can be found in more detail in the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license/overview) legal document. |
| `IMPROPER_PAYMENT` | Refers to missing payments associated with an IP. |
| `CONTENT_STANDARDS_VIOLATION`
Examples: No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography | Refers to "No-Hate", "Suitable-for-All-Ages", "No-Drugs-or-Weapons" and "No-Pornography". These items can be found in more detail in the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license/overview) legal document. |
| `IN_DISPUTE` | Different from the other 4, this is a temporary tag that goes away at the end of a dispute and is replaced by "0x" in case of no infringement or is replaced by one of the other tags. |
## Dispute Process Flow
### Raise Dispute
The `raiseDispute` function is permissionless and allows any address to raise a dispute against any IP Asset registered on the protocol. The dispute initiator has to:
1. Select which "tag" it is raising a dispute on which will be applied to the IP Asset if the arbitration decision is positive. This means an IP Asset is officially "tagged" only when the proposed tag is confirmed as correct ("positive decision" in the diagram above).
2. Submit the dispute evidence for evaluation
3. Other conditions custom to each arbitration policy - such as payment rules, etc.
### Set Dispute Judgement
The `setDisputeJudgement` can only be called by whitelisted addresses and allows the caller to set the dispute judgment. Can only be called once as dispute decisions are immutable. If 3rd parties want to offer the possibility for recourse they can do so on their end and relay the final judgment.
### Tag Derivative If Parent Infringed
If the `setDisputeJudgement` has tagged an IP as infringing then any address can call `tagIfRelatedIpInfringed` to apply the same tag as the parent to the derivatives all the way down the derivative chain or if the IP is a group then the group member tag can be applied to any group IP which it is a member of.
Looking Ahead
In the future, the idea is that any related IP Asset of an infringing IP Asset would automatically be tagged without needing someone to call `tagIfRelatedIpInfringed`. This is currently a limitation that we are aware of.
The derivatives are then tagged directly without any need for judgment given that it is considered that if a parent IP license has been infringed then all derivatives that come from that license are also implicitly in an infringement situation.
**Example**: IPA 7 is first tagged ("PLAGIARISM") as infringing via `setDisputeJudgement` after having gone through a dispute process. Only after that can IPAs 3, 1, and 0 can be tagged via `tagIfRelatedIpInfringed` by any address without needing to go through a new dispute process.
### Resolve Dispute
Resolving a dispute removes the tag from the IP Asset. Since there are two ways in which a tag can be applied, there are two ways for it to be resolved:
1. Tag was applied via the`setDisputeJudgement` function
In a case where a dispute judgment was positive, then a tag was applied. After the tag has been applied to an IP Asset, the **dispute initiator** can, if he/she believes the matter to be resolved and the tag to no longer apply, choose to remove it by calling `resolveDispute`. For example, if one party owed money to the dispute initiator and paid the full amount after the dispute judgment then the tag could be cleared and the IP Asset would have a clean slate again.
If the dispute initiator chooses to not resolve, then the tag that was defined in `setDisputeJudgement` remains in force.
2. Tag was applied via the`tagIfRelatedIpInfringed` function
If an IP has been previously tagged as infringing via `tagIfRelatedIpInfringed`, such tag can be removed via `resolveDispute` in a permissionless way as long as the parent is no longer considered an infringing IP Asset.
This mechanism of permissionless resolving disputes exists to make it easier to propagate down the derivative chain and remove infringement tags from derivative IPs when the parent has resolved its original dispute and is no longer considered as being in an infringing situation, and therefore neither are its derivatives.
If no address chooses to resolve, then the tag that was applied from the parent to the derivative remains in force.
### Cancel Dispute
In a case where a dispute was raised but the matter has been resolved before the dispute judgment, the dispute initiator can cancel the dispute. However, depending on the conditions of each arbitration policy, there may be non-refundable fees that are not recouped on cancellation.
Currently, the [UMA Arbitration
Policy](/concepts/dispute-module/uma-arbitration-policy) does not support
cancelling disputes.
# UMA Arbitration Policy
Source: https://docs.story.foundation/concepts/dispute-module/uma-arbitration-policy
A dispute resolution mechanism using UMA's optimistic oracle
For detailed information on how UMA's dispute resolution works, [visit their
website](https://uma.xyz/).
This arbitration policy is a dispute resolution mechanism that uses UMA’s optimistic oracle to verify disputes. Below we share a high-level overview of how the UMA dispute process works.
## Smart Contract Flow Diagram
The first step to initiate a dispute against an IP Asset is to call the `raiseDispute` function on [DisputeModule.sol](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/modules/dispute/DisputeModule.sol). This function will in turn call `assertTruth` on UMA's `OptimisticOracleV3.sol`. To initiate a dispute the dispute initiator will need to post a bond of at least the minimum bond defined by UMA for the selected currency. Note that this bond will be lost if the dispute is deemed not verifiably correct by the oracle.
UMA will be adjusting the minimum \$IP bond size as the IP price fluctuates. The
correct way to obtain the current minimum bond size is via `getMinimumBond()` on
`OptimisticOracleV3.sol` (OOV3), found on our [aeneid
testnet](https://aeneid.storyscan.io/address/0xABac6a158431edED06EE6cba37eDE8779F599eE4?tab=read_write_contract#0x4360af3d)
and
[mainnet](https://www.storyscan.io/address/0x8EF424F90C6BC1b98153A09c0Cac5072545793e8?tab=read_write_contract#0x4360af3d).
After this step, the dispute will be “open” to be countered/appealed by other users. If there is no counter/appeal, UMA rules define that the IP will be considered to be infringing.
```sol DisputeModule.sol theme={null}
/// @notice Raises a dispute on a given ipId
/// @param targetIpId The ipId that is the target of the dispute
/// @param disputeEvidenceHash The hash pointing to the dispute evidence - this could be an IPFS CID converted to a bytes32 hash. This is the document with the proof that UMA reviewers will potentially read
/// @param targetTag The target tag of the dispute
/// @param data The data to initialize the policy - here you can do abi.encode of liveness, token address and bond amount
/// @return disputeId The id of the newly raised dispute
function raiseDispute(
address targetIpId,
bytes32 disputeEvidenceHash,
bytes32 targetTag,
bytes calldata data
) external returns (uint256 disputeId);
```
After the `raiseDispute` call there is a period of time called "liveness" in which a counter dispute/appeal can be submitted. The liveness period is split in two parts: (i) the first part of the liveness period in which only the IP owner can counter dispute/appeal and (ii) a second part in which any address can counter dispute/appeal - which can be done by calling `disputeAssertion` on `ArbitrationPolicyUMA.sol`. To counter a dispute the caller will need to post a bond of the same amount and currency that was used by the dispute initiator when raising a dispute. Note that this bond will be lost if the original dispute is deemed to be verifiably correct by the oracle.
After this step, the dispute is escalated and will be reviewed by external party UMA.
```sol ArbitrationPolicyUMA.sol theme={null}
/// @notice Allows the IP that was targeted with a dispute to dispute the assertion while providing counter evidence
/// @param assertionId The identifier of the assertion that was disputed
/// @param counterEvidenceHash The hash of the counter evidence
function disputeAssertion(bytes32 assertionId, bytes32 counterEvidenceHash) external;
/// @notice Returns the assertion id for a given dispute id
/// @param disputeId The dispute id
function disputeIdToAssertionId(uint256 disputeId) external view returns (bytes32);
```
UMA reviewers judge the dispute. On this step the user just has to wait until
the UMA reviewers make the dispute judgement. This step could take 48-96
hours.
This step is expected to be automatic as UMA runs a bot that calls `settleAssertion` which in turn distributes the bonds back to the address that wins the dispute.
1. If nobody submitted a counter dispute then when the liveness period is over, any address can call `settleAssertion` on UMA's `OptimisticOracleV3.sol`.
2. If somebody has submitted a counter dispute/appeal before the liveness period is over, then the dispute is escalated to UMA reviewers who will judge and make a decision on whether the IP is infringing or not. After the decision has been made, then any address can call `settleAssertion` on UMA's `OptimisticOracleV3.sol`.
## Dispute Evidence Submission Guidelines
When raising a dispute or making a counter dispute, both parties can submit dispute evidence. Dispute evidence refers to a text document that oracle participants will use & read from to make a judgement on the dispute.
### Burden of Proof
In all disputes with UMA arbitration policy, the burden of proof lies with the party creating the dispute. This means that the disputer must provide clear, compelling, and verifiable evidence to prove the dispute beyond reasonable doubt. Disputes that do not meet this high bar can be counter-disputed with the disputing party losing their bond.
### Document Characteristics
As the process is still experimental, we can expect iteration and fine-tuning on the contents/formats of how the evidence should be submitted.
Every document should have the following characteristics:
* It should be a text document. Can have images or video if necessary.
* It should be uploaded on IPFS.
* It should not take the reviewer more than 1 hour to review the dispute evidence document - the reviewer's time is limited and the evidence could be deemed invalid if it would take too much time to review. Best efforts will be applied to solve a dispute but please keep it concise to have your dispute evidence be valid.
Depending on what the type of the Dispute Tag is, you also need to include in the evidence the "Dispute Evidence Contents of the table below:
| Dispute Tag | Dispute Evidence Contents | Dispute review process (Human reviewer instructions) |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IMPROPER_REGISTRATION` | A. Showcase or pointer to the pre-existing IP that is being infringed upon by the disputed IP
B. Proof of public display of the pre-existing IP at an earlier date than the infringing IP (onchain or offchain) and/or instructions on where/how to check it | 1. Check if the pre-existing is the same or very similar to the disputed IP using input A - Mickey Mouse with 1 pixel difference is an infringement - Mickey Mouse with a new hat is an infringement unless it's a derivative of the original Mickey Mouse with an appropriate license 2. Check the registration date of the pre-existing IP using input B 3. Confirm that the disputed IP has a later registration date 4. Confirm that the disputed IP is not a derivative of the pre-existing IP
|
| `IMPROPER_USAGE`
Examples (non-exhaustive): Territory, Channels of Distribution, Expiration, Irrevocable, Attribution, Derivatives, Limitations on Creation of Derivatives, Commercial Use, Sublicensable, Non-Transferable, Restriction on Cross-Platform Use | A. PIL term that has been violated
B. Description of the violation
C. Proof of the violation | 1. Read the associated PIL term description on the PIL license official document using input A 2. Read the violation description using input B 3. Decide on the veracity of the proof presented by checking on associated platforms when possible using input C
|
| `IMPROPER_PAYMENT` | A. Description of each payment the disputed IP received that should have been shared with its royalty vault and/or its ancestors but it were not
B. Proof of those payments that were not properly shared as royalties | 1. Check veracity of the proof of payments by checking on the associated platforms when possible using input A and B 2. If proof of payments are deemed to be real, confirm that the payment has indeed not been made onchain by checking on the blockchain explorer. Payments should be made calling payRoyaltyOnBehalf() function on RoyaltyModule.sol smart contract. In addition, royalty payments must be made within 15 days of when the capital was originally received by the owner/IP who is paying those royalties.
|
| `CONTENT_STANDARDS_VIOLATION`
No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography | A. The content standard point that has been violated
B. Description of the violation
C. Proof of violation | 1. Read the associated content standards description on the official content standards section in the PIL using input A 2. Read the violation description using input B 3. Decide on the veracity of the proof presented by checking on associated platforms when possible using input C
|
# Concepts FAQ
Source: https://docs.story.foundation/concepts/faq
Common technical questions related to our protocol documentation.
## *"What is the difference between License Tokens, Royalty Tokens, and Revenue Tokens?"*
| | License Tokens | Royalty Tokens | Revenue Tokens |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Module** | [📜 Licensing Module](/concepts/licensing-module) | [💸 Royalty Module](/concepts/royalty-module) | [💸 Royalty Module](/concepts/royalty-module) |
| **Explanation** | An ERC-721 NFT that gets minted from an IP Asset with specific license terms. It is essentially the license you hold that gives you access to use the associated IP Asset based on the terms in the License Token.
A License Token is burned when it is used to register an IP Asset as a derivative of another. | Each IP Asset has 100,000,000 Royalty Tokens associated, where each token represents the right of whoever owns them to claim 0.000001% of the gains ("*Revenue Tokens*") deposited into the IPA's Royalty Vault. | These are the tokens that are actually used for payment (ex. \$WIP).
"*Royalty Tokens*" are used to claim these Revenue Tokens when an IP Asset earns them. |
| **Associated Docs** | [License Token](/concepts/licensing-module/license-token) | [IP Royalty Vault](/concepts/royalty-module/ip-royalty-vault) | [IP Royalty Vault](/concepts/royalty-module/ip-royalty-vault) |
# 👥 Grouping Module
Source: https://docs.story.foundation/concepts/grouping-module
Create and manage group IP Assets, supporting a royalty pool for the group.
The Grouping Module enables the creation and management of group IP Assets, supporting a royalty pool for the group.
View the smart contract for the Grouping Module.
`GroupingModule.sol` is the main entry point for the grouping workflows. It is **stateless** and responsible for:
* Registering a new group
* Adding an IPA to a group
* Removing an IPA from a group
* Checking whether a group contains a specific IPA
* Get the total number of IPAs of a group
## Creating a Group IPA
Similar to the IP Asset registration process, in which you must have a minted NFT to register and then an IP Account is created, the same applies to Group IP Assets. You must have a minted ERC-721 NFT (that represents the ownership of the group) to register as a group, and then when you register, an IP Account for the group is deployed.
Anyone can create a new group.
### Group IP Asset Registry
Similar to how when an IP Asset is created an IP Account is deployed & registered through the [IP Asset Registry](/concepts/registry/ip-asset-registry), the Group's IP Account is deployed and registered through the [Group IP Asset Registry](/concepts/registry/group-ip-asset-registry). This is responsible for managing the registration and tracking of Group IP Assets, including the group members and reward pools.
### The Group's IP Account
The Group IP Account should function equivalently to a normal IP Account, allowing attachment of license terms, creation of derivatives, execution with modules, and other interactions. It also has the same common interface of IP Account. Hence, the Group IP Account can be applied to anywhere where IP Account can be applied.
Besides the common interfaces of IP Account, the Group IP Account has functions to manage the adding/removing of individual IPAs in the group.
## Group Restrictions
Here are the restrictions associated with a Group IPA:
* A derivative IP of a group IP can only have the group IP as its sole parent
* A group IP cannot attach License Terms that use the [Liquid Absolute Percentage (LAP)](/concepts/royalty-module/liquid-absolute-percentage) royalty policy
* An empty group cannot have derivative IPs or mint License Tokens
* A group IP cannot be registered as a derivative
* A group IP can only attach one license term common to all members
* Once a Group gains its first member, the `mintingFee`, `licensingHook`, and `licensingHookData` are frozen. The Group's `commercialRevShare` can only increase
* A group has a maximum size of 1000 members
### Adding & Removing from a Group
* Only the owner of a group can add/remove IP Assets. You **do not** have to own an IP Asset to add it to your group.
* An IPA must include one license terms that matches the license terms of the group (same `licenseTemplate` and `licenseTerms`. An IPA may include other license terms in addition to the one that matches the group.
* When adding an IP to a Group, the Group and IP must have the same `mintingFee` and `licenseHook` in the `LicenseConfig`. Additionally, the Group's commercial revenue share must be greater than or equal to the IP's commercial revenue share.
### Groups Becoming Locked
When a group is locked, IPAs cannot be removed from it, but new IPAs can still be added.
A group IPA is locked when:
1. it has a derivative IP registered OR
2. when someone mints a license token from the group
## Example
Let's say you have an AI bot that uses training data to continuously learn and produce better content. The training data is a Group IPA that is the root, and the AI bot is a derivative IPA of the training data. And any time the AI bot gets paid, the revenue flows back to the training data as revenue.
Now you want to add more training data to the group. Since the group is now locked (you linked a derivative to it), you should register a new Group IPA as a root, and then a new AI bot as a derivative.
# 🪝 Hooks
Source: https://docs.story.foundation/concepts/hooks
Add custom logic before minting license tokens or registering derivatives.
Hooks allow developers to create custom implementations, restrictions, and functionality upon minting [License Tokens](/concepts/licensing-module/license-token) or registering derivatives.
There are two types of hooks:
1. **Licensing Hooks**: allow you to [add custom logic before minting license tokens](/concepts/licensing-module/license-config#logic-that-is-possible-with-license-config) (and registering derivatives). For example, requesting a dynamic price, limiting the amount of license tokens that can be minted, whitelists, etc. Licensing Hooks can be added / modified on a licensing config at any point.
2. **Commercializer Checker Hooks**: similar to Licensing Hooks, however they are directly a part of the license terms and do not change. You also cannot return a custom minting fee.
## Licensing Hooks
These are contracts that implement the `ILicensingHook` interface, which extends from `IModule`.
Most importantly, a Licensing Hook implements a `beforeMintLicenseTokens` function, which is a function that is called before a License Token is minted to implement [custom logic](/concepts/licensing-module/license-config#logic-that-is-possible-with-license-config) and determine the final `totalMintingFee` of that License Token.
View the `ILicensingHook` smart contract
[here](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/interfaces/modules/licensing/ILicensingHook.sol#L26).
```solidity ILicensingHook.sol theme={null}
/// @notice This function is called when the LicensingModule mints license tokens.
/// @dev The hook can be used to implement various checks and determine the minting price.
/// The hook should revert if the minting is not allowed.
/// @param caller The address of the caller who calling the mintLicenseTokens() function.
/// @param licensorIpId The ID of licensor IP from which issue the license tokens.
/// @param licenseTemplate The address of the license template.
/// @param licenseTermsId The ID of the license terms within the license template,
/// which is used to mint license tokens.
/// @param amount The amount of license tokens to mint.
/// @param receiver The address of the receiver who receive the license tokens.
/// @param hookData The data to be used by the licensing hook.
/// @return totalMintingFee The total minting fee to be paid when minting amount of license tokens.
function beforeMintLicenseTokens(
address caller,
address licensorIpId,
address licenseTemplate,
uint256 licenseTermsId,
uint256 amount,
address receiver,
bytes calldata hookData
) external returns (uint256 totalMintingFee);
```
Note that it returns the `totalMintingFee`. You may be wondering, "I can set the minting fee in the License Terms, in the `LicenseConfig`, and return a dynamic price from `beforeMintLicenseTokens`. What will the final minting fee actually be?" Here is the priority:
| Minting Fee | Importance |
| ------------------------------------------------------------- | ---------------- |
| The `totalMintingFee` returned from `beforeMintLicenseTokens` | Highest Priority |
| The `mintingFee` set in the `LicenseConfig` | ⬇️ |
| The `mintingFee` set in the License Terms | Lowest Priority |
Beware of potentially malicious implementations of external license hooks.
Please first verify the code of the hook you choose because it may be not
reviewed or audited by the Story team.
### Available Hooks
Below are available hooks deployed on our protocol that you can use.
View the deployed addresses for these hooks [here](/developers/deployed-smart-contracts#license-hooks).
| Hook | Description | Contract Code |
| :------------------------- | :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------ |
| LockLicenseHook | Stop the minting of license tokens or registering new derivatives. | [View here ↗️](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/hooks/LockLicenseHook.sol) |
| TotalLicenseTokenLimitHook | Set a limit on the amount of license tokens that can be minted, updatable at any time. | [View here ↗️](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/hooks/TotalLicenseTokenLimitHook.sol) |
### Implementing the Hooks
A working TypeScript SDK code example that shows how to implement a licensing
hook. More specifically, how to limit the # of licenses that can be minted.
A working Solidity code example that shows how to implement a licensing hook.
More specifically, how to limit the # of licenses that can be minted.
Licensing Hooks are ultimately a smart contract that implements the `ILicensingHook` interface. You can view the interface [here](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/interfaces/ILicensingHook.sol). We have a few Licensing Hooks deployed already (view the chart above).
In order to actually use a Licensing Hook, you must set it in the Licensing Config, which is basically a set of configurations that you set on License Terms when attaching terms to an IP Asset.
First you have to create a Licensing Config:
```typescript {6-8} theme={null}
import { LicensingConfig } from '@story-protocol/core-sdk';
const licensingConfig: LicensingConfig = {
isSet: true,
mintingFee: 0n,
// address of TotalLicenseTokenLimitHook
// from https://docs.story.foundation/developers/deployed-smart-contracts
licensingHook: '0xaBAD364Bfa41230272b08f171E0Ca939bD600478',
hookData: zeroAddress,
commercialRevShare: 0,
disabled: false,
expectMinimumGroupRewardShare: 0,
expectGroupRewardPool: zeroAddress,
}
```
Next, we'll set the Licensing Config on the License Terms. In the following example, we'll show this happening upon registering the IP Asset:
This code snippet requires a bit of setup, and it meant for developers who
already understand how to setup the TypeScript SDK. If you want to learn more,
check out the [working code
example](https://github.com/storyprotocol/typescript-tutorial/blob/main/scripts/licenses/oneTimeUseLicense.ts).
This uses the `registerIpAsset` method found [here](/sdk-reference/ipasset#registeripasset).
```typescript {6-7} theme={null}
const response = await client.ipAsset.registerIpAsset({
nft: {
type: 'mint',
spgNftContract: '0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc', // public spg contract for ease-of-use
},
licenseTermsData: [
{
terms: { defaultMintingFee: 0, commercialUse: true, ... }, // dummy license terms
// set the licensing config here
licensingConfig: licensingConfig
},
],
ipMetadata: {
ipMetadataURI: 'test-uri',
ipMetadataHash: toHex('test-metadata-hash', { size: 32 }),
nftMetadataHash: toHex('test-nft-metadata-hash', { size: 32 }),
nftMetadataURI: 'test-nft-uri',
}
})
console.log(`Token ID: ${response.tokenId}, IPA ID: ${response.ipId}, License Terms ID: ${response.licenseTermsIds}`);
```
Now that we have set the Licensing Config on our terms, we can call the `setTotalLicenseTokenLimit` function on the hook and set the max # of licenses that can be minted to 1.
```typescript theme={null}
const hookResponse = await client.license.setMaxLicenseTokens({
ipId: response.ipId,
licenseTermsId: response.licenseTermsIds![0],
maxLicenseTokens: 1000,
});
console.log(`Max license tokens set at transaction hash ${hookResponse.txHash}`);
```
### Creating a New Licensing Hook
Please follow the below process for creating a new licensing hook and getting it whitelisted into Story's protocol.
1. **Develop your Hook**: You may fork [this template repository](https://github.com/storyprotocol/hook-dev-template) to bootstrap your development. It includes an example hook with tests against our Aeneid protocol, but using it is optional.
See
[license-caller-whitelist-hook](https://github.com/jacob-tucker/license-caller-whitelist-hook)
as an example that was used to develop the `LicenseCallerWhitelistHook.sol`
hook.
2. **Fork Registered Modules Repository**: Fork the [registered-modules repository](https://github.com/storyprotocol/registered-modules) to your own GitHub account.
3. **Update the Module List**: In your `registered-modules` repository, add your hook's details to the `hook-modules.json` file. Make sure to **deploy & verify on block explorer** your hook on both aeneid and mainnet. Ensure that you adhere to the following JSON structure:
```json theme={null}
{
"name": "YourModuleName",
"aeneid": {
"address": "YourModuleAddress",
"blockExplorerLink": "YourModuleBlockExplorerLink"
},
"mainnet": {
"address": "YourModuleAddress",
"blockExplorerLink": "YourModuleBlockExplorerLink"
}
}
```
Replace `YourModuleName`, `YourModuleAddress`, and `YourModuleBlockExplorerLink` with your hook's name, its address, and the link to its block explorer page, respectively.
Example:
```json theme={null}
{
"name": "LicenseCallerWhitelistHook",
"aeneid": {
"address": "0x37be56d9fb06d885cda3cb010096c94c28b4d658",
"blockExplorerLink": "https://aeneid.storyscan.io/address/0x37be56d9fb06d885cda3cb010096c94c28b4d658?tab=contract"
},
"mainnet": {
"address": "0x6d9d51a444c8318e8840e75dab7ed81b5a714610",
"blockExplorerLink": "https://www.storyscan.io/address/0x6d9d51a444c8318e8840e75dab7ed81b5a714610?tab=contract"
}
}
```
4. **Create a Pull Request (PR)**: Once you have added your hook, create a pull request against this repository. In your PR's description, add the following information (replace the values with your own):
```md theme={null}
## Register my module
- Module type: `hook`
- Module name: `LicenseCallerWhitelistHook`
- Aeneid Module address: `0x37be56d9fb06d885cda3cb010096c94c28b4d658`
- Mainnet Module address: `0x6d9d51a444c8318e8840e75dab7ed81b5a714610`
- My module is immutable: yes
- My module is using an upgradeable proxy: no
- My module has been verified on the block explorer (required): yes
- Summary of my module: Hook for allowing a licensor to gate which addresses can mint a license. The licensor can add/remove an address at any time.
- GitHub repository with source code and tests: https://github.com/jacob-tucker/license-caller-whitelist-hook
```
5. **Await Verification**: After your PR is submitted, it will be reviewed. Once a security audit has been performed and completed, and the module whitelisted into the protocol, the PR will be merged. At this point your module will be officially registered and recognized as safe for use within the Story community.
We look forward to seeing your contributions and expanding the Story module ecosystem!
## Commercializer Checker Hooks
Documentation coming soon. If you have questions in the meantime, ask in the [Builder's Discord](https://discord.gg/storybuilders).
# ⚙️ IP Account
Source: https://docs.story.foundation/concepts/ip-asset/ip-account
A modified ERC-6551 implementation bound to an IP Asset
Skip the Read
Get a quick 2-minute overview of IP Accounts [here](https://twitter.com/jacobmtucker/status/1787603252198134234).
When an [🧩 IP Asset](/concepts/ip-asset/overview) is registered, it is given an associated **IP Account**. An IP Account is a modified ERC-6551 (Token Bound Account) implementation. It is a separate contract bound to the IP Asset for controlling permissions around interactions with Story's modules or storing the IP's associated data. Upon registration, an IP Asset is assigned a unique ID. This ID is the address of the IP Account that is bound to the IP Asset.
An IP Account mainly does two things:
1. Stores comprehensive IP-related data, including metadata and ownership details of associated assets such as the License Tokens or Royalty Tokens that are created from the IP.
2. Facilitates the utilization of this data by various modules. These modules interact with and contribute to the IP Account, creating and storing data. For example, licensing, revenue/royalty sharing, remixing, disputing an IP, and other modules are made possible due to the IP Account's programmability.
If the underlying NFT is transferred, the new owner is also automatically the owner of the associated IP Asset & IP Account.
## `execute` and `executeWithSig`
A key feature of IP Account is the generic `execute()` function, which allows calling arbitrary modules within Story via encoded bytes data (thus extensible for future modules). Additionally, there is a `executeWithSig()` function that enables users to sign transactions and have others execute on their behalf for seamless UX.
# 📝 IPA Metadata Standard
Source: https://docs.story.foundation/concepts/ip-asset/ipa-metadata-standard
An overview of the IP-specific metadata standard
We are still figuring out the best way to define an IPA Metadata Standard. For
the sake of transparency, the following document is our thoughts so far but is
subject to change as we release future versions.
Check out the official Ippy IP, which has both NFT & IP metadata.
Learn how to actually add the IP metadata discussed here to your IP Asset with an explanation or completed code example.
This is the JSON metadata that is associated with an IP Asset, and gets stored inside of an IP Account. You must call `setMetadata(...)` inside of the IP Account in order to set the metadata, and then call `metadata()` to read it.
## Attributes & Structure
Below are the important attributes you should provide in your IP metadata. Under the **Required For** column is what the specific field is required for:
* 🔍 Story Explorer - this field will help display your IP on the Story Explorer
* 🕵️ [Commercial Infringement Check](/concepts/story-attestation-service) - this field is required if your IP is **commercial** (that is, has `commercialUse = true` license terms attached). We will use these fields to run an infringement check on your IP.
* See [current limitations](/concepts/story-attestation-service#current-limitations).
* 🤖 AI Agents - used for displaying metadata associated with AI Agents
| Property Name | Type | Description | Required For |
| ------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `title` | `string` | Title of the IP | 🔍 Story Explorer |
| `description` | `string` | Description of the IP | 🔍 Story Explorer |
| `createdAt` | `string` | Date/Time that the IP was created (either ISO8601 or unix format). This field can be used to specify historical dates that aren't on-chain. For example, Harry Potter was published on June 26. | 🔍 Story Explorer |
| `image` | `string` | An image for your IP. **For audio assets, the recommended thumbnail aspect ratio is 1:1. For video assets, it is 16:9.** | 🔍 Story Explorer |
| `imageHash` | `string` | Hash of your `image` using SHA-256 hashing algorithm. See [here](#hashing-content) for how that is done. | 🔍 Story Explorer |
| `creators` | `IpCreator[]` | An array of information about the creators. [See the type defined below](#type-definitions) | 🔍 Story Explorer |
| `mediaUrl` | `string` | Used for infringement checking, points to the actual media (ex. image or audio). **For audio assets, the recommended thumbnail aspect ratio is 1:1. For video assets, it is 16:9.** | 🕵️ [Commercial Infringement Check](/concepts/story-attestation-service) |
| `mediaHash` | `string` | Hashed string of the media using SHA-256 hashing algorithm. See [here](#hashing-content) for how that is done. | 🕵️ [Commercial Infringement Check](/concepts/story-attestation-service) |
| `mediaType` | `string` | Type of media (audio, video, image), based on [mimeType](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types). See the allowed media types [here](#media-types). | 🕵️ [Commercial Infringement Check](/concepts/story-attestation-service) |
| `aiMetadata` | `AIMetadata` | Used for registering & displaying AI Agent Metadata. [See the type defined below](#type-definitions) | 🤖 AI Agents |
| N/A | N/A | You can include other values as well. | N/A |
### Type Definitions
Here are the type definitions for the complex types used in the metadata:
```typescript IpCreator theme={null}
type IpCreator = {
name: string;
address: Address;
contributionPercent: number; // add up to 100
description?: string;
image?: string;
socialMedia?: IpCreatorSocial[];
role?: string;
};
type IpCreatorSocial = {
platform: string;
url: string;
};
```
```typescript AIMetadata theme={null}
type AIMetadata = {
// this can be any character file you want
// example: https://github.com/elizaOS/characterfile/blob/main/examples/example.character.json
characterFileUrl: string;
characterFileHash: string;
};
```
### Media Types
The following media types are allowed for the `mediaType` field:
| Media Type | Description |
| ----------------- | --------------------- |
| `image/jpeg` | JPEG image |
| `image/png` | PNG image |
| `image/apng` | Animated PNG image |
| `image/avif` | AV1 Image File Format |
| `image/gif` | GIF image |
| `image/svg+xml` | SVG image |
| `image/webp` | WebP image |
| `audio/wav` | WAV audio |
| `audio/mpeg` | MP3 audio |
| `audio/flac` | FLAC audio |
| `audio/aac` | AAC audio |
| `audio/ogg` | OGG audio |
| `audio/mp4` | MP4 audio |
| `audio/x-aiff` | AIFF audio |
| `audio/x-ms-wma` | WMA audio |
| `audio/opus` | Opus audio |
| `video/mp4` | MP4 video |
| `video/webm` | WebM video |
| `video/quicktime` | QuickTime video |
### Hashing Content
To hash content for the `imageHash` or `mediaHash` fields, you can use the SHA-256 hashing algorithm. Here's an example of how to do this in JavaScript:
```typescript TypeScript theme={null}
import { toHex, Hex } from "viem";
// get hash from a file
async function getFileHash(file: File): Promise {
const arrayBuffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer);
return toHex(new Uint8Array(hashBuffer), { size: 32 });
}
// get hash from a url
async function getHashFromUrl(url: string): Promise {
const response = await axios.get(url, { responseType: "arraybuffer" });
const buffer = Buffer.from(response.data);
return "0x" + createHash("sha256").update(buffer).digest("hex");
}
```
```shell Shell theme={null}
shasum -a 256 myfile.jpg
```
### Example Use Cases
This is the official Ippy mascot that is registered on mainnet. You can view it on our protocol explorer [here](https://explorer.story.foundation/ipa/0xB1D831271A68Db5c18c8F0B69327446f7C8D0A42).
```json theme={null}
{
"title": "Ippy",
"description": "Official mascot of Story.",
"createdAt": "1728401700",
"image": "https://ipfs.io/ipfs/QmSamy4zqP91X42k6wS7kLJQVzuYJuW2EN94couPaq82A8",
"imageHash": "0x21937ba9d821cb0306c7f1a1a2cc5a257509f228ea6abccc9af1a67dd754af6e",
"mediaUrl": "https://ipfs.io/ipfs/QmSamy4zqP91X42k6wS7kLJQVzuYJuW2EN94couPaq82A8",
"mediaHash": "0x21937ba9d821cb0306c7f1a1a2cc5a257509f228ea6abccc9af1a67dd754af6e",
"mediaType": "image/png",
"creators": [
{
"name": "Story Foundation",
"address": "0x67ee74EE04A0E6d14Ca6C27428B27F3EFd5CD084",
"description": "The World's IP Blockchain",
"contributionPercent": 100,
"socialMedia": [
{
"platform": "Twitter",
"url": "https://twitter.com/storyprotocol"
},
{
"platform": "Telegram",
"url": "https://t.me/StoryAnnouncements"
},
{
"platform": "Website",
"url": "https://story.foundation"
},
{
"platform": "Discord",
"url": "https://discord.gg/storyprotocol"
},
{
"platform": "YouTube",
"url": "https://youtube.com/@storyFDN"
}
]
}
],
"tags": ["Ippy", "Story", "Story Mascot", "Mascot", "Official"], // experimental field
"ipType": "Character" // experimental field
}
```
This is an example song generated on [Suno](https://suno.com/) and registered on our testnet. View the below example [on our protocol explorer](https://aeneid.explorer.story.foundation/ipa/0x7d126DB8bdD3bF88d757FC2e99BFE3d77a55509b).
```json theme={null}
{
"title": "Midnight Marriage",
"description": "This is a house-style song generated on suno.",
"createdAt": "1740005219",
"creators": [
{
"name": "Jacob Tucker",
"address": "0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112",
"contributionPercent": 100
}
],
"image": "https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg",
"imageHash": "0xc404730cdcdf7e5e54e8f16bc6687f97c6578a296f4a21b452d8a6ecabd61bcc",
"mediaUrl": "https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3",
"mediaHash": "0xb52a44f53b2485ba772bd4857a443e1fb942cf5dda73c870e2d2238ecd607aee",
"mediaType": "audio/mpeg"
}
```
The main difference here is you should supply `aiMetadata` with a character file. You can provide any character file you want, or use [this ElizaOS example](https://github.com/elizaOS/characterfile/blob/main/examples/example.character.json) as a template.
View the below example [on our protocol explorer](https://aeneid.explorer.story.foundation/ipa/0x49614De8b2b02C790708243F268Af50979D568d4).
```json theme={null}
{
"title": "Story AI Agent",
"description": "This is an example AI Agent registered on Story.",
"createdAt": "1740005219",
"creators": [
{
"name": "Jacob Tucker",
"address": "0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112",
"contributionPercent": 100
}
],
"image": "https://ipfs.io/ipfs/bafybeigi3k77t5h5aefwpzvx3uiomuavdvqwn5rb5uhd7i7xcq466wvute",
"imageHash": "0x64ccc40de203f218d16bb90878ecca4338e566ab329bf7be906493ce77b1551a",
"mediaUrl": "https://ipfs.io/ipfs/bafybeigi3k77t5h5aefwpzvx3uiomuavdvqwn5rb5uhd7i7xcq466wvute",
"mediaHash": "0x64ccc40de203f218d16bb90878ecca4338e566ab329bf7be906493ce77b1551a",
"mediaType": "image/webp",
"aiMetadata": {
"characterFileUrl": "https://ipfs.io/ipfs/bafkreic6eu4hlnwx46soib62rgkhhmlieko67dggu6bzk7bvtfusqsknfu",
"characterFileHash": "0x5e253875b6d7e7a4e407da899473b168229def8cc6a783957c35996928494d2d"
}
}
```
## Optional Properties
The following properties are optional but can provide additional context about your IP Asset:
We are still figuring out the best way to define an IPA Metadata Standard. The
fields below are bound to change or be removed at some point.
| Property Name | Type | Description |
| :--------------- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ipType` | `string` | Type of the IP Asset, can be defined arbitrarily by the creator. I.e. "character", "chapter", "location", "items", "music", etc |
| `relationships` | `IpRelationship[]` | The detailed relationship info with the IPA's direct parent asset, such as `APPEARS_IN`, `FINETUNED_FROM`, etc. See more examples [here](#relationship-types). |
| `watermarkImage` | `string` | A separate image with your watermark already applied. This way apps choosing to use it can render this version of the image (with watermark applied). |
| `media` | `IpMedia[]` | An array of supporting media. Media type defined below |
| `app` | `StoryApp` | This is assigned to verified application from Story directly (on a request basis so far). We will map each App ID to a name |
| `tags` | `string[]` | Any tags that can help surface this IPA |
| `robotTerms` | `IPRobotTerms` | Allows you to set Do Not Train for a specific agent |
| N/A | N/A | You can include other values as well. |
### Type Definitions
```typescript IpRelationship theme={null}
type IpRelationship = {
parentIpId: Address;
type: string; // see "Relationship Types" docs below
};
```
```typescript IpMedia theme={null}
type IpMedia = {
name: string;
url: string;
mimeType: string;
};
```
```typescript StoryApp theme={null}
type StoryApp = {
id: string;
name: string;
website: string;
action?: string;
};
```
```typescript IPRobotTerms theme={null}
type IPRobotTerms = {
userAgent: string;
allow: string;
};
```
### Relationship Types
The different relationship types that can be used for the `relationships` attribute.
#### Story Relationships
1. **APPEARS\_IN** - A character APPEARS\_IN a chapter.
2. **BELONGS\_TO** - A chapter BELONGS\_TO a book.
3. **PART\_OF** - A book is PART\_OF a series.
4. **CONTINUES\_FROM** - A chapter CONTINUES\_FROM the previous one.
5. **LEADS\_TO** - An event LEADS\_TO a consequence.
6. **FORESHADOWS** - An event FORESHADOWS future developments.
7. **CONFLICTS\_WITH** - A character CONFLICTS\_WITH another character.
8. **RESULTS\_IN** - A decision RESULTS\_IN a significant change.
9. **DEPENDS\_ON** - A subplot DEPENDS\_ON the main plot.
10. **SETS\_UP** - A prologue SETS\_UP the story.
11. **FOLLOWS\_FROM** - A chapter FOLLOWS\_FROM the previous one.
12. **REVEALS\_THAT** - A twist REVEALS\_THAT something unexpected occurred.
13. **DEVELOPS\_OVER** - A character DEVELOPS\_OVER the course of the story.
14. **INTRODUCES** - A chapter INTRODUCES a new character or element.
15. **RESOLVES\_IN** - A conflict RESOLVES\_IN a particular outcome.
16. **CONNECTS\_TO** - A theme CONNECTS\_TO the main narrative.
17. **RELATES\_TO** - A subplot RELATES\_TO the central theme.
18. **TRANSITIONS\_FROM** - A scene TRANSITIONS\_FROM one setting to another.
19. **INTERACTED\_WITH** - A character INTERACTED\_WITH another character.
20. **LEADS\_INTO** - An event LEADS\_INTO the climax.?\
**PARALLEL - story** happening in parallel or around the same timeframe
#### AI Relationships
1. **TRAINED\_ON** - A model is TRAINED\_ON a dataset.
2. **FINETUNED\_FROM** - A model is FINETUNED\_FROM a base model.
3. **GENERATED\_FROM** - An image is GENERATED\_FROM a fine-tuned model.
4. **REQUIRES\_DATA** - A model REQUIRES\_DATA for training.
5. **BASED\_ON** - A remix is BASED\_ON a specific workflow.
6. **INFLUENCES** - Sample data INFLUENCES model output.
7. **CREATES** - A pipeline CREATES a fine-tuned model.
8. **UTILIZES** - A workflow UTILIZES a base model.
9. **DERIVED\_FROM** - A fine-tuned model is DERIVED\_FROM a base model.
10. **PRODUCES** - A model PRODUCES generated images.
11. **MODIFIES** - A remix MODIFIES the base workflow.
12. **REFERENCES** - An AI-generated image REFERENCES original data.
13. **OPTIMIZED\_BY** - A model is OPTIMIZED\_BY specific algorithms.
14. **INHERITS** - A fine-tuned model INHERITS features from the base model.
15. **APPLIES\_TO** - A fine-tuning process APPLIES\_TO a model.
16. **COMBINES** - A remix COMBINES elements from multiple datasets.
17. **GENERATES\_VARIANTS** - A model GENERATES\_VARIANTS of an image.
18. **EXPANDS\_ON** - A fine-tuning process EXPANDS\_ON base capabilities.
19. **CONFIGURES** - A workflow CONFIGURES a model’s parameters.
20. **ADAPTS\_TO** - A fine-tuned model ADAPTS\_TO new data.
# IP Modifications & Restrictions
Source: https://docs.story.foundation/concepts/ip-asset/ipa-modifications
Learn about the modifications and restrictions for IP Assets
# IP Asset Modifications
IP Assets can be modified/customized a few ways. For example, by [setting the License Config](/concepts/licensing-module/license-config) which allows you to change a few things as you'll see below, changing its metadata, and more. These things can **always be changed unless there is a certain condition**.
| Action | Conditions | Via The... |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Modify License Minting Fee | You can **increase** the minting fee. You **cannot** decrease it. | [License Config](/concepts/licensing-module/license-config) |
| Modify Licensing Hook | The hook must be whitelisted in the [Module Registry](/concepts/registry/module-registry). | [License Config](/concepts/licensing-module/license-config) |
| Modify `commercialRevShare` | You can **increase** the rev share percentage. You **cannot** decrease it.
However, you **can** set it to 0 to disable the overwrite. | [License Config](/concepts/licensing-module/license-config) |
| Disable/Enable the License | License can be disabled or re-enabled at any time.
*Note that disabling a license disallows future licenses from being minted, but does not affect existing ones.* | [License Config](/concepts/licensing-module/license-config) |
| Modify Metadata | Cannot modify if the metadata is **frozen**. This is done by calling `freezeMetadata` in the [CoreMetadataModule.sol](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/modules/metadata/CoreMetadataModule.sol). | [CoreMetadataModule.sol](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/modules/metadata/CoreMetadataModule.sol) |
## License Hook Modifications
The IP can be further customized or modified using the [License Hook](/concepts/hooks#licensing-hooks). This is a function that gets set within the License Config that gets called before a [License Token](/concepts/licensing-module/license-token) (or more simply, a "license") is minted. There are various features you can implement with the License Hook, and are **always modifiable**:
| Feature | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Dynamic License Fee | You can dynamically set the price of a license. For example, it can be updated dynamically via bonding curve logic. |
| Total # of Licenses | You can abort the function based on a maximum number of license tokens that can be minted. |
| Specific Receivers | You can restrict minting of license to a specific receiver. |
| More... | Additional licensing hook features can be implemented as required. |
# Group IPA Restrictions
See [👥 Group IPA Restrictions](/concepts/grouping-module#group-restrictions) for more information.
# 🧩 IP Asset
Source: https://docs.story.foundation/concepts/ip-asset/overview
The foundational programmable IP metadata on Story
Skip the Read
Get a quick 1-minute overview of IP Assets [here](https://twitter.com/jacobmtucker/status/1785765362744889410).
IP Assets are the foundational programmable IP metadata on Story. Each IP Asset is an on-chain ERC-721 NFT (representing an IP). If your IP is off-chain, you would simply mint an ERC-721 NFT to represent that IP first, and then register it as an IP Asset.
When an IP Asset is created, an associated [⚙️ IP Account](/concepts/ip-asset/ip-account) is deployed, which is a modified ERC-6551 (Token Bound Account) implementation. It is a separate contract bound to the IP Asset for controlling permissions around interactions with Story's modules or storing the IP's associated data.
## Registering an IP Asset
An IP Asset is created by registering an ERC-721 NFT into Story's global [IP Asset Registry](/concepts/registry/ip-asset-registry).
If you'd like to jump into code examples/tutorials, please see [How to Register IP on Story](/developers/tutorials/how-to-register-ip-on-story).
## NFT vs. IP Metadata
On Story, your IP is an NFT that gets registered on the protocol as an IP Asset. However, both NFTs and IP Assets have their own metadata you can set, so what's the difference?
| | Standard | What is it? |
| :------ | :------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------- |
| **NFT** | [Opensea ERC721 Standard](https://docs.opensea.io/docs/metadata-standards) | Things like `name`, `description`, `image`, `attributes`, `animation_url`, etc |
| **IP** | [📝 IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard) | More specific to Story, this includes necessary information about the underlying content for infringement checks, authors of the work, etc |
All other metadata, such as the ownership, legal, and economic details of an IP Asset are handled by our protocol directly. For example, the protocol stores data associated with parent-child relationships through the [📜 Licensing Module](/concepts/licensing-module/overview), the monetary flow between IP Assets through the [💸 Royalty Module](/concepts/royalty-module/overview), and the legal constraints/permissions of an IP Asset with the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license/overview).
### Adding NFT & IP Metadata to IP Asset
Jump to the code and see a completed code example of adding NFT & IP metadata to an IP Asset
Learn how to add metadata to your IP Asset with a step-by-step explanation.
In practice, whether you are using the SDK or our smart contract directly, our protocol asks you to provide 4 different parameters:
* View the `WorkflowStructs.sol` contract [here](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/lib/WorkflowStructs.sol).
```solidity WorkflowStructs.sol theme={null}
/// @notice Struct for metadata for NFT minting and IP registration.
/// @dev Leave the nftMetadataURI empty if not minting an NFT.
/// @param ipMetadataURI The URI of the metadata for the IP.
/// @param ipMetadataHash The hash of the metadata for the IP.
/// @param nftMetadataURI The URI of the metadata for the NFT.
/// @param nftMetadataHash The hash of the metadata for the IP NFT.
struct IPMetadata {
string ipMetadataURI;
bytes32 ipMetadataHash;
string nftMetadataURI;
bytes32 nftMetadataHash;
}
```
* `ipMetadataURI` - a URI pointing to a JSON object that follows the [📝 IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard)
* `ipMetadataHash` - hash of the `ipMetadataURI` JSON object
* `nftMetadataURI` - a URI pointing to a JSON object that follows the [Opensea ERC721 Standard](https://docs.opensea.io/docs/metadata-standards)
* `nftMetadataHash` - hash of the `nftMetadataURI` JSON object
# License Config
Source: https://docs.story.foundation/concepts/licensing-module/license-config
An optional config that can be attached to a specific license for dynamic minting fees and custom logic.
## License Config
View the LicensingConfig struct in the smart contract.
Optionally, you can attach a `LicensingConfig` to an IP Asset (for a specific `licenseTermsId` attached to that asset) which contains fields like a `mintingFee` and a `licensingHook`, as shown below.
```solidity theme={null}
/// @notice This struct is used by IP owners to define the configuration
/// when others are minting license tokens of their IP through the LicensingModule.
/// When the `mintLicenseTokens` function of LicensingModule is called, the LicensingModule will read
/// this configuration to determine the minting fee and execute the licensing hook if set.
/// IP owners can set these configurations for each License or set the configuration for the IP
/// so that the configuration applies to all licenses of the IP.
/// If both the license and IP have the configuration, then the license configuration takes precedence.
/// @param isSet Whether the configuration is set or not.
/// @param mintingFee The minting fee to be paid when minting license tokens.
/// @param licensingHook The hook contract address for the licensing module, or address(0) if none
/// @param hookData The data to be used by the licensing hook.
/// @param commercialRevShare The commercial revenue share percentage.
/// @param disabled Whether the license is disabled or not.
/// @param expectMinimumGroupRewardShare The minimum percentage of the group's reward share
/// (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group.
/// If the remaining reward share in the group is less than the minimumGroupRewardShare,
/// the IP cannot be added to the group.
/// @param expectGroupRewardPool The address of the expected group reward pool.
/// The IP can only be added to a group with this specified reward pool address,
/// or address(0) if the IP does not want to be added to any group.
struct LicensingConfig {
bool isSet;
uint256 mintingFee;
address licensingHook;
bytes hookData;
uint32 commercialRevShare;
bool disabled;
uint32 expectMinimumGroupRewardShare;
address expectGroupRewardPool;
}
```
What do some of these mean?
1. `isSet` - if this is false, the whole licensing config is completely ignored. So for example, if the licensing config has `mintingFee == 10` and `disabled == true`, but the `isSet == false`, the `mintingFee` and `disabled` will be completely ignored.
2. `disabled` - if this is true, then no licenses can be minted and no more derivatives can be attached at all for the terms the config is attached to.
Fields like the `mintingFee` and `commercialRevShare` overwrite their duplicate in the license terms themselves. **A benefit of this is that derivative IP Assets, which normally cannot change their license terms, are able to overwrite certain fields.**
The `licensingHook` is an address to a smart contract that implements the `ILicensingHook` interface, which contains a `beforeMintLicenseTokens` function which will be run before a user mints a License Token. This means you can insert logic to be run upon minting a license.
The hook itself is described in a different section. You can see it contains information about the license, who is minting the License Token, and who is receiving it.
Learn all about Licensing Hooks [here](/concepts/hooks#licensing-hooks).
### Setting the License Config
You can set the License Config by calling the `setLicenseConfig` function in the [LicensingModule.sol contract](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/modules/licensing/LicensingModule.sol).
### Logic that is Possible with License Config
1. **Max Number of Licenses**: The `licensingHook` (described in the next section) is where you can define logic for the max number of licenses that can be minted. For example, reverting the transaction if the max number of licenses has already been minted.
2. **Disallowing Derivatives**: If you register a derivative of an IP Asset, that derivative cannot change its License Terms as described [here](/concepts/licensing-module/license-terms#inherited-license-terms). You can be wondering: "What if I, as a derivative, want to disallow derivatives of myself, but my License Terms allow derivatives and I cannot change this?" To solve this, you can simply set `disabled` to true.
3. **Minting Fee**: Similar to #2 above... what about the minting fee? Although you cannot change License Terms on a derivative IP Asset (and thus the minting fee inside of it), you can change the minting fee for that derivative by modifying the `mintingFee` in the License Config, or returning a `totalMintingFee` from the `licensingHook` (described in the next section).
4. **Commercial Revenue Share**: Similar to #2 and #3 above, you can modify the `commercialRevShare` in the License Config.
5. **Dynamic Pricing for Minting a License Token**: Set dynamic pricing for minting a License Token from an IP Asset based on how many total have been minted, how many licenses the user is minting, or even who the user is. All of this data is available in the `licensingHook` (described in the next section).
... and more.
### Restrictions
See [IP Modifications & Restrictions](/concepts/ip-asset/ipa-modifications) for the various restrictions on setting the License Config.
# License Template
Source: https://docs.story.foundation/concepts/licensing-module/license-template
A legal framework, written in code ("programmable"), that defines various licensing terms for an IP
A License Template is a legal framework, written in code ("programmable"), that defines various licensing terms for an IP. Such as:
* "Is commercial use allowed?" - true/false (bool)
* "Is the license transferrable?" - true/false (bool)
* "If commercial, what % of royalty do I receive?" - number
These terms and values differ per License Template.
The first (and currently only) example of a License Template was developed by the Story team directly, and is called the Programmable IP License (PIL :pill:).
Learn about the first implementation of a License Template
View the smart contract for the PIL.
## License Template Requirements
License Templates are responsible for:
* Providing a link to the actual, off-chain, legal contract template, with all the parameters, their possible values, and the correspondent legalese, in `licenseTextUrl`.
* For a licensing framework to be compatible with Story, the legal text **must** be clear and parametrized, with each licensing parameter establishing the possible outcomes of each value.
* The parameter values in each License Template (called "License Template terms") drive the legal text for each license agreement.
* Defining a `struct` with the particular definitions of the parameters in accordance, which must be encoded into the License Terms struct (described below).
* Providing registration methods for the License Terms, and getters.
* **Verifying** that both the **minter** and the address **linking a derivative are allowed by the License Template terms to perform those actions**.
* These conditions could be enforced by the License Template itself or through hooks. They can range from limitations on the derivative creations, token-gating LNFT holders, creative control from licensors, KYC, etc. It's up to the implementation of each License Template.
* **Verifying that the License Terms are compatible if a derivative has or will have multiple parents**
## Create Your Own Template
You can create your own License Template (like the PIL), but it must be approved by the Story team to be fully embedded into the protocol.
# License Terms
Source: https://docs.story.foundation/concepts/licensing-module/license-terms
A particular combination of values from a License Template that define how others can interact with your IP
When registering your IP on Story, you can attach License Terms to the IP. These are real, legally binding terms enforced on-chain by the [📜 Licensing Module](/concepts/licensing-module/overview), disputable by the [❌ Dispute Module](/concepts/dispute-module/overview), and in the worst case, able to be enforced off-chain in court through traditional means.
In them are also terms for commercial usage, which describes how the [💸 Royalty Module](/concepts/royalty-module/overview) will be enforced (ex. "50% of revenue must be shared with the parent IP").
View some popular combinations of PIL License Terms, also known as
"flavors".
More specifically, License Terms are a particular combination of values from a [License Template](/concepts/licensing-module/license-template). Indeed, there can and will exist **multiple** License Terms (variations) for each License Template. You can imagine that a License Template generates many License Term variations.
Once registered, **License Terms are immutable — they can't be tampered with or altered**, even by the License Template that generated it.
Additionally, License Terms have a unique numeric ID within the License Template they stem from. This makes License Terms reusable, meaning if someone creates License Terms with a specific set of values, it only needs to be created once and can be used by anyone else.
For example, a particular set of term values of the [Programmable IP License (PIL💊)](/concepts/programmable-ip-license/overview), such as non-commercial usage + derivatives allowed + free minting, defines a unique License Terms with an associated ID.
## License Terms Attached to IP Asset
The owner of a root IP Asset can attach License Terms to signal to other users that they can mint License Tokens of those terms to create a derivative of this IP Asset. **Once License Terms are attached to an IP Asset, it is now considered "public" and anyone can mint a License Token using those terms.**
## Inherited License Terms
On the other hand, derivative IP Assets inherit their License Terms from the parent IP Asset. This means that when an IP Asset registers itself as a derivative, it burns the License Token and inherits the associated License Terms. **The owner of this derivative cannot set new License Terms.**
You may be wondering: "if I cannot set new License Terms on my derivative, does that also mean I can't change the minting fee, or disallowing more derivatives, on my derivative?"
Thankfully, there is a way to get around this! Although you cannot change License Terms on a derivative IP, you can utilize the [License Config to implement special behaviors](/concepts/licensing-module/license-config).
## Expiration
License Terms support an `expiration` time. Once License Terms expire, any derivatives that abide by that license will no longer be able to generate revenue or create further derivatives. If an IP Asset is a derivative of multiple parents, it will expire when the soonest expiration time between the two parents is reached.
# License Token
Source: https://docs.story.foundation/concepts/licensing-module/license-token
An ERC-721 NFT that allows you to register your IP as a derivative of another, based on the License Terms defined in the token
View the smart contract for License Tokens.
A **License Token** is represented as an **ERC-721 NFT** and contains the specific [License Terms](/concepts/licensing-module/license-terms) it represents. Its associated `licenseTokenId` is global, as there is one License Token contract.
Once License Terms are attached to an IP Asset, it becomes public so that anyone can mint a License Token for those terms. A License Token is burned when it is used to register another IP as a derivative of the original IP Asset.
## Private Licenses
In order to mint a private License Token, the owner of a root IP Asset can issue License Tokens that have terms **not yet attached to the IP Asset itself**. It is important to also note that derivative IP Assets cannot issue private licenses because it is restricted to only issue licenses of its inherited terms.
## Transferability of the License Token
License Tokens might be transferrable or not, depending on the values of the License Terms terms they point to.
Once a non-transferable License Token is minted to a recipient, it is locked there forever.
## Registering a Derivative
You can register an IP Asset as a derivative of other IP Assets, each with their own license terms agreement. This creates a legally binding agreement between IP Assets that enforces things things like automatic payments in the [💸 Royalty Module](/concepts/royalty-module/overview).
### ⚠️ Restrictions
There are a few restrictions on registering a derivative:
* An IP Asset can only register as a derivative one time. If an IP Asset has multiple parents, it must register both at the same time.
* Once an IP Asset is a derivative, it cannot link any more parents.
* When you link an IP Asset as a derivative, it cannot have license terms attached. It will inherit its terms from its parents.
* None of the parent IP Assets or the child IP Asset can be disputed.
* The child IP Asset cannot have derivatives already.
* If at least one of the license terms is commercial, then they all must be commercial (`commercialUse = true`)
***
There are two ways to register a derivative IP Asset.
An IP Asset can only register as a derivative one time. If an IP Asset has multiple parents, it must register both at the same time. Once an IP Asset is a derivative, it cannot link any more parents.
### 1. Using an Existing License Token
A License Token is burned when it is used to register another IP as a derivative of the original IP Asset.
### 2. Registering a Derivative Directly
You can also register a derivative directly, without the need for a License Token. Remember that if License Terms are attached to an IP Asset it is public to mint the License Token anyway, so this is simply a convenient way to go about it, thus skipping the middle step of minting a License Token.
# 📜 Licensing Module
Source: https://docs.story.foundation/concepts/licensing-module/overview
Learn about creating & attaching real legal license to your IP on Story
The Licensing Module allows you to create a real legal license from a **License Template** (which is the [Programmable IP License (PIL💊)](/concepts/programmable-ip-license/overview)) and attach it to your IP Asset. This license, and the **License Terms** that define it, restrict how others can use your IP, commercialize it, and remix it.
If License Terms are attached to an IP Asset, anyone can mint a **License Token** (an ERC-721 NFT) from it which acts as the license to use that work based on the terms that define it. This token can then be burned to register a derivative work. This then establishes a parent-child relationship between assets, unlocking things like automatic royalty flow from the [💸 Royalty Module](/concepts/royalty-module/overview).
The owner of an IP Asset owns intellectual property rights such as creating derivatives, being commercially exploited, and being reproduced in different platforms.
IP Assets can programmatically grant permissions for any users to exercise those rights with some autonomy via [License Tokens](/concepts/licensing-module/license-token) (an ERC-721 NFT), which point to a particular set of conditions, known as [License Terms](/concepts/licensing-module/license-terms).
## LicensingModule
View the smart contract for the License Module.
The `LicensingModule.sol` contract is the main entry point for the licensing system. It is responsible for:
* Attaching License Terms to IP Assets
* Minting License Tokens
* Registering derivatives
* Setting License Configs
## Further Readings
The following document will walk through all of the major components of the Licensing Module as shown above:
* [License Template](/concepts/licensing-module/license-template)
* [License Terms](/concepts/licensing-module/license-terms)
* [License Token](/concepts/licensing-module/license-token)
* [License Registry](/concepts/registry/license-registry)
* [License Config](/concepts/licensing-module/license-config)
# 👀 Metadata Module
Source: https://docs.story.foundation/concepts/metadata-module
Manage & view metadata for IP Assets.
The Metadata Module enables the creation, management, and retrieval of metadata for IP Assets within Story. It consists of two main components: the CoreMetadataModule for writing operations and the CoreMetadataViewModule for reading operations.
View the smart contract for the Core Metadata Module.
View the smart contract for the Core Metadata View Module.
## Metadata Structure
The metadata for an IP Asset includes:
* **metadataURI**: A URI pointing to the detailed metadata of the IP Asset
* **metadataHash**: A hash of the metadata for verification purposes
* **nftTokenURI**: A URI pointing to the metadata of the NFT associated with the IP Asset
* **nftMetadataHash**: A hash of the NFT metadata for verification
* **registrationDate**: When the IP Asset was registered
* **owner**: The current owner of the IP Asset
## CoreMetadataModule (Write Operations)
`CoreMetadataModule.sol` is responsible for writing and updating metadata for IP Assets. It is **stateful** and provides the following key functionalities:
* Setting and updating metadata URIs for IP Assets
* Setting and updating NFT token URIs
* Freezing metadata to make it immutable
* Managing metadata hashes for verification
The module stores metadata in the IP Asset's storage, making it accessible to other modules and applications.
### Setting Metadata
To set metadata for an IP Asset, the caller must have appropriate permissions. The CoreMetadataModule provides several functions for setting metadata:
* `setMetadataURI`: Sets just the IP metadataURI and its hash
* `updateNftTokenURI`: Updates the NFT token URI and its hash
* `setAll`: Sets all metadata attributes at once
Here is an example:
```solidity solidity theme={null}
// Set the metadata URI and hash
coreMetadataModule.setMetadataURI(
ipAssetAddress,
"https://example.com/metadata/asset123",
keccak256("metadata content hash")
);
```
### Freezing Metadata
The CoreMetadataModule allows IP Asset owners to freeze metadata, making it immutable. Once frozen, the metadata cannot be changed, ensuring the permanence of the IP Asset's information.
To freeze metadata:
```solidity solidity theme={null}
// Make the metadata immutable
coreMetadataModule.freezeMetadata(ipAssetAddress);
```
You can check if metadata is frozen using:
```solidity solidity theme={null}
// Check if metadata is frozen
bool isFrozen = coreMetadataModule.isMetadataFrozen(ipAssetAddress);
```
## CoreMetadataViewModule (Read Operations)
`CoreMetadataViewModule.sol` is a read-only module that provides access to the metadata stored by the CoreMetadataModule. It follows the View Module pattern and offers these key functionalities:
* Retrieving metadata URIs and hashes
* Retrieving NFT token URIs and metadata hashes
* Generating formatted JSON strings with all metadata attributes
* Checking registration dates and ownership information
### Retrieving Metadata
The CoreMetadataViewModule provides various functions to retrieve metadata:
* `getCoreMetadata`: Returns all metadata in a single struct
* `getMetadataURI`: Returns just the metadata URI
* `getNftTokenURI`: Returns the NFT token URI
* `getJsonString`: Returns a formatted JSON string with all metadata
Here is an example:
```solidity solidity theme={null}
// Get the metadata URI
string memory uri = coreMetadataViewModule.getMetadataURI(ipAssetAddress);
// Get all metadata in one call
CoreMetadata memory metadata = coreMetadataViewModule.getCoreMetadata(ipAssetAddress);
// Get a JSON representation of all metadata
string memory jsonMetadata = coreMetadataViewModule.getJsonString(ipAssetAddress);
```
The Metadata Module provides a robust system for managing IP Asset metadata, ensuring that important information about intellectual property is properly recorded, accessible, and can be made immutable when needed.
# 🧱 Modules
Source: https://docs.story.foundation/concepts/modules/overview
Learn about the standalone contracts that take action on IP Assets
Modules are standalone contracts that adhere to the [`IModule` interface](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/interfaces/modules/base/IModule.sol). These modules play a crucial role in taking action on IP to change the data/state around or of IP.
## Existing Modules
There are a few important modules, created by the Story team, to be aware of:
| Module | Description |
| ------------------------------------------------- | -------------------------------------------------------------------------------------- |
| [📜 Licensing Module](/concepts/licensing-module) | Responsible for defining and attaching licenses to IP Assets. |
| [💸 Royalty Module](/concepts/royalty-module) | Responsible for handling royalty flow between parent & child IP Assets. |
| [❌ Dispute Module](/concepts/dispute-module) | Responsible for handling the dispute of wrongfully registered or misbehaved IP Assets. |
| [👥 Grouping Module](/concepts/grouping-module) | Responsible for handling groups of IPAs. |
| [👀 Metadata Module](/concepts/metadata-module) | Manage and view metadata for IP Assets. |
## Base Module
The Base Module provides a standard set of must-have functionalities for all modules registered on Story. Anyone wishing to create and register a module on Story must inherit and override the Base Module.
View the smart contract [here](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/modules/BaseModule.sol).
### Key Features
#### Simplicity and Flexibility
The BaseModule is intentionally kept simple and generalized. It only implements the ERC165 interface, which is crucial for interface detection. This design choice allows for maximum flexibility when developing more specific modules within Story.
#### ERC165 Interface Implementation
By implementing the ERC165 interface, BaseModule allows other contracts to query whether it supports a specific interface. This feature is essential for ensuring compatibility and interoperability within the Story ecosystem and beyond.
```solidity theme={null}
abstract contract BaseModule is ERC165, IModule {
...
}
```
#### `supportsInterface` Function
A key function in the BaseModule is `supportsInterface`, which overrides the ERC165's `supportsInterface` method. This function is crucial for interface detection, allowing the contract to declare support for both its own `IModule` interface and any other interfaces it might inherit.
```solidity theme={null}
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return interfaceId == type(IModule).interfaceId || super.supportsInterface(interfaceId);
}
```
# Overview
Source: https://docs.story.foundation/concepts/overview
A broad overview of Story's "Proof-of-Creativity" protocol.
A piece of Intellectual Property is represented as an [🧩 IP Asset](/concepts/ip-asset) and its associated [⚙️ IP Account](/concepts/ip-asset/ip-account), a smart contract designed to serve as the core identity for each IP. We also have various [🧱 Modules](/concepts/modules) to add functionality to IP Assets, like creating derivatives of them, disputing IP, and automating revenue flow between them.
Let's briefly introduce the layers mentioned in the above diagram:
## [🧩 IP Asset](/concepts/ip-asset)
When you want to bring an IP on-chain, you mint an ERC-721 NFT. This NFT represents **ownership** over your IP.
Then, you **register** the NFT in our protocol through the [IP Asset Registry](/concepts/registry/ip-asset-registry). This deploys an [⚙️ IP Account](/concepts/ip-asset/ip-account), effectively creating an "IP Asset". The address of that contract is the identifier for the IP Asset (the `ipId`).
The underlying NFT can be traded/sold like any other NFT, and the new owner will own the IP Asset and all revenue associated with it.
## [⚙️ IP Account](/concepts/ip-asset/ip-account)
IP Accounts are smart contracts that are tied to an IP Asset, and do two main things:
1. Store the associated IP Asset's data, such as the associated licenses and royalties created from the IP
2. Facilitates the utilization of this data by various modules. For example, licensing, revenue/royalty sharing, remixing, and other critical features are made possible due to the IP Account's programmability.
The address of the IP Account is the IP Asset's identifier (the `ipId`).
## [🧱 Modules](/concepts/modules)
Modules are customizable smart contracts that define and extend the functionality of IP Accounts. Modules empower developers to create functions and interactions for each IP to make IPs truly programmable.
We already have a few core modules:
1. [📜 Licensing Module](/concepts/licensing-module): create parent\<->child relationships between IPs, enabling derivatives of IPs that are restricted by the agreements in the license terms (must give attribution, share 10% revenue, etc)
2. [💸 Royalty Module](/concepts/royalty-module): automate revenue flow between IPs, abiding by the negotiated revenue sharing in license terms
3. [❌ Dispute Module](/concepts/dispute-module): facilitates the disputing and flagging of IP
4. [👥 Grouping Module](/concepts/grouping-module): allows for IPs to be grouped together
5. [👀 Metadata Module](/concepts/metadata-module): manage and view metadata for IP Assets
## [🗂️ Registry](/concepts/registry)
The various registries on our protocol function as a primary directory/storage for the global states of the protocol. Unlike IP Accounts, which manage the state of specific IPs, a registry oversees the broader states of the protocol.
## [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license)
The PIL is a real, off-chain legal contract that defines certain **License Terms** for how an IP Asset can be legally licensed. For example, how an IP Asset is commercialized, remixed, or attributed, and who is allowed to do that and under what conditions.
We have mapped these same terms on-chain so you can easily attach terms to your IP Asset for others to seamlessly and transparently license your IP.
# How does Story protect IP?
Source: https://docs.story.foundation/concepts/programmable-ip-license/how-does-story-protect-ip
Okay... how does Story *actually* protect IP?
Every license created on Story is a real, enforceable legal contract.
## The Programmable IP License (PIL): The Foundation of Legal Enforcement
At its core, every [IP Asset](/concepts/ip-asset) registered on Story is wrapped by a **legally binding document called the [Programmable IP License (PIL)](/concepts/programmable-ip-license)**. Based on US copyright law, the PIL acts as a universal license agreement template that allows IP owners to attach customizable terms to their assets.
**Licensing on Story means making a genuine legal commitment.** The parameters defined in the PIL—commercial use, derivative allowances, attribution requirements, and royalty structures—represent legally enforceable terms between the IP owner (licensor) and anyone who licenses the IP (licensee).
### How does the PIL enable enforcement?
* **Clear Legal Terms:** The PIL provides a standardized way for IP owners to define usage rules.
* **On-Chain Record as Evidence:** PIL terms attached to an IP Asset are recorded immutably on Story's purpose-built blockchain, serving as *irrefutable evidence* of the agreed-upon terms.
* **Off-Chain Legal Recourse:** If IP is misused in violation of PIL terms, the IP owner can leverage on-chain evidence in **off-chain legal proceedings** such as arbitration or court actions.
* **License Tokens as Proof of Rights:** Licensees receive a [**License Token**](/concepts/licensing-module/license-token) (NFT) representing specific usage rights granted under the PIL terms, providing further evidence of authorization status.
## The Story Attestation Service (SAS): Proactive Infringement Monitoring
Beyond the legal framework, we are building the [**Story Attestation Service (SAS)**](/concepts/story-attestation-service) to help IP owners monitor for potential copyright infringement using a multi-layered decentralized approach.
SAS is a signal layer, not a judgment layer—it flags potential issues for the
IP owner to act upon, rather than taking automated enforcement actions.
### How the SAS Helps with Infringement Detection:
* **Network of Specialized Providers:** SAS coordinates with service providers like Yakoa and Pex that use AI and machine learning to detect copyright violations across different media types on the internet and other blockchains.
* **Transparent Signals:** SAS provides publicly accessible signals regarding the legitimacy of an IP Asset based on provider findings.
* **Focus on Commercial IP:** Currently, SAS primarily runs infringement checks on commercial IP Assets—those with at least one License Terms where `commercialUse = true`.
* **Metadata-Driven Checks:** SAS relies on IP-specific metadata provided during registration to perform checks against existing online content.
### Important Considerations:
* **Detection, Not Prevention:** SAS primarily flags potential infringements after IP registration rather than preventing them.
* **Internet-Based Checks:** Currently, SAS primarily detects infringement based on content already existing online, not offline uses.
* **No Guarantee of Perfection:** No system can guarantee 100% detection of all copyright infringement.
## The Role of the Dispute Module
We have also built a [**Dispute Module**](/concepts/dispute-module) that allows anyone to raise on-chain disputes against IP Assets for reasons such as improper registration or potential plagiarism. This can lead to on-chain flagging of disputed IP, potentially affecting its ability to generate licenses or earn revenue.
## The Hybrid Enforcement Model
Story doesn't replace courts or lawyers—it gives IP holders tools that work
with traditional enforcement systems while benefiting from on-chain
automation, transparency, and interoperability.
### What Story Can Do:
* Provide a legally sound framework for IP licensing through the PIL
* Create an immutable on-chain record of IP ownership and license terms
* Offer monitoring tools through the SAS to detect potential online infringement
* Facilitate on-chain dispute resolution through the Dispute Module
* Provide evidence usable for off-chain legal enforcement
### What Story Cannot Do:
* Act as a global police force for IP infringement
* Guarantee prevention of all unauthorized IP uses
* Directly enforce legal judgments in the physical world
* Monitor every digital and physical interaction with registered IP
```
+--------------------------+ +-----------------------------+
| IP Owner Registers IP on |----->| IP Asset Created on Story |
| Story | | (with associated metadata) |
+--------------------------+ +-----------------------------+
|
v
+---------------------------------------+ +-------------------------+
| Programmable IP License (PIL) |<--| IP Owner Attaches Legal |
| (Legal wrapper defining usage terms) | | Terms via PIL |
+---------------------------------------+ +-------------------------+
|
v
+-------------------------+
| IP Asset with PIL Terms |
| (Commercial Use = true) |
+-------------------------+
|
v
+--------------------------+ +-------------------------------------+
| Story Attestation |----->| SAS Providers Scan Internet & Other |
| Service (SAS) Coordinates| | Sources for Infringement (using IP |
+--------------------------+ | Metadata) |
+-------------------------------------+
|
v
+----------------------------------------------------------------------+
| SAS Providers Report Potential Infringement Signals for the IP Asset |
| (e.g., "Potential copy found on website X") |
+----------------------------------------------------------------------+
|
v
+---------------------------------------------------------------------+
| IP Owner Reviews SAS Signals on IP Portal (Coming Soon) |
+---------------------------------------------------------------------+
|
v
+---------------------------------------------------------------------+
| IP Owner Can Use SAS Signals & PIL Terms as Evidence for: |
| - On-Chain Dispute via Dispute Module |
| - Off-Chain Legal Action (e.g., Cease & Desist, Lawsuit) |
+---------------------------------------------------------------------+
```
# 💊 Programmable IP License (PIL)
Source: https://docs.story.foundation/concepts/programmable-ip-license/overview
Story Programmable IP License - A legal framework for IP licensing on-chain
The PIL is a legal off-chain document based on US copyright law created by the Story team.
The parameters outlined in the PIL (ex. "Commercial Use", "Derivatives Allowed", etc) have been mapped on-chain, which means they can be enforced on-chain via our protocol, bridging code and law and unlocking the benefit of transparent, autonomous, and permission-less smart contracts for the world of intellectual property.
Check out the actual PIL legal text. It is very human-readable for a legal
text!
The PIL is the first and currently only example of a [License Template](/concepts/licensing-module/license-template). A License Template is simply a traditional legal document that has been brought on-chain and contains a set of pre-defined terms that people must set, like:
* `commercialUse` - can someone use my work commercially?
* `mintingFee` - the cost of minting a license to use my work in your own works.
* `derivativesAttribution` - does someone have to credit me in their derivative works?
In code, these terms form a struct that represent their legal off-chain counterparts. To see all of the terms defined by the PIL and their associated explanations in code, see [PIL Terms](/concepts/programmable-ip-license/pil-terms).
To see example configurations ("flavors") of the PIL, see [PIL Flavors (examples)](/concepts/programmable-ip-license/pil-flavors).
## The Background Story
If you just want to get started developing with the PIL, you can skip this section.
We designed Story's [📜 Licensing Module](/concepts/licensing-module/overview) to power the expansion of emerging forms of creativity, such as authorized remixes and co-creation. Our protocol can support any media format or project, ranging from user-generated social videos & images to Hollywood-grade collaborative storytelling.
Intellectual property owners can permit other parties to use, or build on, their work by granting rights in a license, which can be for profit or for the common good. In the media world, these licenses are generally highly tailored contracts, which vary by media formats and the unique needs of licensors - often requiring unique expertise (via lawyers) and significant resources to create.
We searched for a form of a "universal license" that could support these emerging activities at scale. Hat tip to [Creative Commons](https://creativecommons.org/mission/), [Arweave](https://mirror.xyz/0x64eA438bd2784F2C52a9095Ec0F6158f847182d9/AjNBmiD4A4Sw-ouV9YtCO6RCq0uXXcGwVJMB5cdfbhE), A16Z / [Can't Be Evil,](https://a16zcrypto.com/posts/article/introducing-nft-licenses/) The [Token-Bound NFT License](https://james.grimmelmann.net/files/articles/token-bound-nft-license.pdf) and music rights organizations, among others. But we simply couldn't find one framework or agreement robust enough - so with our expert legal counsel (with special thanks to Ghaith Mahmood and Heather Liu) we created one ourselves! **Introducing the Programmable IP License (PIL:pill:)**, the first example of a [License Template](/concepts/licensing-module/license-template) on the protocol.
## Feedback
We are excited to collect feedback and collaborate with IP owners to unlock the potential of their works - please let us know what you think! We can be reached at `legal@storyprotocol.xyz`.
Check out the actual PIL legal text. It is very human-readable for a legal
text!
# PIL Flavors (examples)
Source: https://docs.story.foundation/concepts/programmable-ip-license/pil-flavors
Pre-configured License Terms for ease of use
The [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license/overview) is very configurable, but we support popular pre-configured License Terms (also known as "flavors") for ease of use. We expect these to be the most popular options:
## Non-Commercial Social Remixing
This flavor is already registered as `licenseTermsId = 1` on our protocol. This is because it doesn't take any inputs, so we registered it ahead of time.
Let the world build on and play with your creation. This license allows for endless free remixing while tracking all uses of your work while giving you full credit. Similar to: TikTok plus attribution.
### What others can do?
| Others can | Others cannot |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------- |
| ✅ Remix this work (`derivativesAllowed == true`) | ❌ Commercialize the original and derivative works (`commercialUse == false`) |
| ✅ Distribute their remix anywhere | ❌ Claim credit for any derivative works (`derivativesAttribution == true`) |
| ✅ Get the license for free (`defaultMintingFee == 0`) | ❌ Claim credit for the original work ("Attribution" is true in the off-chain terms) |
### PIL Term Values
* **On-chain**:
```solidity Solidity theme={null}
PILTerms({
transferable: true,
royaltyPolicy: address(0),
defaultMintingFee: 0,
expiration: 0,
commercialUse: false,
commercialAttribution: false,
commercializerChecker: address(0),
commercializerCheckerData: EMPTY_BYTES,
commercialRevShare: 0,
commercialRevCeiling: 0,
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: 0,
currency: address(0),
uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/NCSR.json"
});
```
```typescript TypeScript theme={null}
import { zeroAddress } from "viem";
import { LicenseTerms } from "@story-protocol/core-sdk";
const nonCommercialSocialRemix: LicenseTerms = {
transferable: true,
royaltyPolicy: zeroAddress,
defaultMintingFee: 0n,
expiration: 0n,
commercialUse: false,
commercialAttribution: false,
commercializerChecker: zeroAddress,
commercializerCheckerData: "0x",
commercialRevShare: 0,
commercialRevCeiling: 0n,
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: 0n,
currency: zeroAddress,
uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/NCSR.json",
};
```
* **Off-chain:**
| Parameter | Options / Tags |
| --------------------------------- | --------------------------------------------------------------------------- |
| Territory | No restrictions |
| Channels of Distribution | No Restriction |
| Attribution | True |
| Content Standards | No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography |
| Sublicensable | False |
| AI Learning Models | False |
| Restriction on Cross-Platform Use | False |
| Governing Law | California, USA |
| Alternative Dispute Resolution | Tag: Alternative-Dispute-Resolution Ledger-Authoritative-Dispute-Resolution |
| Additional License Parameters | None |
## Commercial Use
Retain control over reuse of your work, while allowing anyone to appropriately use the work in exchange for the economic terms you set. This is similar to Shutterstock with creator-set rules.
### What others can do?
| Others can | Others cannot |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| ✅ Commercialize the original work (`commercialUse == true`) | ❌ Remix this work (`derivativesAllowed == false`) |
| ✅ Keep all revenue (`commercialRevShare == 0`) | ❌ Claim credit for the original work (`commercialAttribution == true`) |
| | ❌ Get the license for free (`defaultMintingFee` is set) |
| | ❌ Claim credit for the original work even non-commercially ("Attribution" is true in the off-chain terms) |
### PIL Term Values
* **On-chain**:
```solidity Solidity theme={null}
PILTerms({
transferable: true,
royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address
defaultMintingFee: MINTING_FEE, // ex. 1000000000000000000 (which means it costs 1 $WIP to mint)
expiration: 0,
commercialUse: true,
commercialAttribution: true,
commercializerChecker: address(0),
commercializerCheckerData: EMPTY_BYTES,
commercialRevShare: 0,
commercialRevCeiling: 0,
derivativesAllowed: false,
derivativesAttribution: false,
derivativesApproval: false,
derivativesReciprocal: false,
derivativeRevCeiling: 0,
currency: CURRENCY, // ex. $WIP address
uri: "https://github.com/piplabs/pil-document/blob/9a1f803fcf8101a8a78f1dcc929e6014e144ab56/off-chain-terms/CommercialUse.json"
})
```
```typescript TypeScript theme={null}
import { zeroAddress, parseEther } from "viem";
import { LicenseTerms } from "@story-protocol/core-sdk";
const commercialUse: LicenseTerms = {
transferable: true,
royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address
defaultMintingFee: MINTING_FEE, // ex. parseEther("1") (which means it costs 1 $WIP to mint)
expiration: 0n,
commercialUse: true,
commercialAttribution: true,
commercializerChecker: zeroAddress,
commercializerCheckerData: "0x",
commercialRevShare: 0,
commercialRevCeiling: 0n,
derivativesAllowed: false,
derivativesAttribution: false,
derivativesApproval: false,
derivativesReciprocal: false,
derivativeRevCeiling: 0n,
currency: CURRENCY, // ex. $WIP address
uri: "https://github.com/piplabs/pil-document/blob/9a1f803fcf8101a8a78f1dcc929e6014e144ab56/off-chain-terms/CommercialUse.json",
};
```
* **Off-chain**
| Parameter | Options / Tags |
| --------------------------------- | --------------------------------------------------------------------------- |
| Territory | No restrictions |
| Channels of Distribution | No Restriction |
| Attribution | True |
| Content Standards | No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography |
| Sublicensable | False |
| AI Learning Models | False |
| Restriction on Cross-Platform Use | False |
| Governing Law | California, USA |
| Alternative Dispute Resolution | Tag: Alternative-Dispute-Resolution Ledger-Authoritative-Dispute-Resolution |
| Additional License Parameters | None |
## Commercial Remix
Let the world build on and play with your creation... and earn money together from it! This license allows for endless free remixing while tracking all uses of your work while giving you full credit, with each derivative paying a percentage of revenue to its "parent" IP.
### Example
Check out Story's official mascot **Ippy**, which we have registered with commercial remix terms on both [Mainnet](https://explorer.story.foundation/ipa/0xB1D831271A68Db5c18c8F0B69327446f7C8D0A42) and [Aeneid Testnet](https://aeneid.explorer.story.foundation/ipa/0x641E638e8FCA4d4844F509630B34c9D524d40BE5).
### What others can do?
| Others can | Others cannot |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| ✅ Remix this work (`derivativesAllowed == true`) | ❌ Claim credit for the original work (`commercialAttribution == true`) |
| ✅ Commercialize the original and derivative works (`commercialUse == true`) | ❌ Claim credit for any derivative works (`derivativesAttribution == true`) |
| ✅ Distribute their remix anywhere | ❌ Keep all revenue (`commercialRevShare` is set) |
| | ❌ Get the license for free (`defaultMintingFee` is set) |
| | ❌ Claim credit for the original work even non-commercially ("Attribution" is true in the off-chain terms) |
### PIL Term Values
* **On-chain**:
```solidity Solidity theme={null}
PILTerms({
transferable: true,
royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address
defaultMintingFee: MINTING_FEE, // ex. 1000000000000000000 (which means it costs 1 $WIP to mint)
expiration: 0,
commercialUse: true,
commercialAttribution: true,
commercializerChecker: address(0),
commercializerCheckerData: EMPTY_BYTES,
commercialRevShare: COMMERCIAL_REV_SHARE, // ex. 50 * 10 ** 6 (which means 50% of derivative revenue)
commercialRevCeiling: 0,
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: 0,
currency: CURRENCY, // ex. $WIP address
uri: "https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json"
});
```
```typescript TypeScript theme={null}
import { zeroAddress, parseEther } from "viem";
import { LicenseTerms } from "@story-protocol/core-sdk";
const commercialRemix: LicenseTerms = {
transferable: true,
royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address
defaultMintingFee: MINTING_FEE, // ex. parseEther("1") (which means it costs 1 $WIP to mint)
expiration: 0n,
commercialUse: true,
commercialAttribution: true,
commercializerChecker: zeroAddress,
commercializerCheckerData: "0x",
commercialRevShare: COMMERCIAL_REV_SHARE, // ex. 50 (which means 50% of derivative revenue)
commercialRevCeiling: 0n,
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: 0n,
currency: CURRENCY, // ex. $WIP address
uri: "https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json",
};
```
* **Off-chain**
| Parameter | Options / Tags |
| --------------------------------- | --------------------------------------------------------------------------- |
| Territory | No restrictions |
| Channels of Distribution | No Restriction |
| Attribution | True |
| Content Standards | No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography |
| Sublicensable | False |
| AI Learning Models | False |
| Restriction on Cross-Platform Use | False |
| Governing Law | California, USA |
| Alternative Dispute Resolution | Tag: Alternative-Dispute-Resolution Ledger-Authoritative-Dispute-Resolution |
| Additional License Parameters | None |
## Creative Commons Attribution
Let the world build on and play with your creation - including making money.
### What others can do?
| Others can | Others cannot |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| ✅ Remix this work (`derivativesAllowed == true`) | ❌ Claim credit for the original work (`commercialAttribution == true`) |
| ✅ Commercialize the original and derivative works (`commercialUse == true`) | ❌ Claim credit for any derivative works (`derivativesAttribution == true`) |
| ✅ Distribute their remix anywhere | ❌ Claim credit for the original work even non-commercially ("Attribution" is true in the off-chain terms) |
| ✅ Get the license for free (`defaultMintingFee == 0`) | |
| ✅ Keep all revenue (`commercialRevShare == 0`) | |
### PIL Term Values
* **On-chain**:
```solidity Solidity theme={null}
PILTerms({
transferable: true,
royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address
defaultMintingFee: 0,
expiration: 0,
commercialUse: true,
commercialAttribution: true,
commercializerChecker: address(0),
commercializerCheckerData: EMPTY_BYTES,
commercialRevShare: 0,
commercialRevCeiling: 0,
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCelling: 0,
currency: CURRENCY, // ex. $WIP address
uri: 'https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/CC-BY.json'
});
```
```typescript TypeScript theme={null}
import { zeroAddress } from "viem";
import { LicenseTerms } from "@story-protocol/core-sdk";
const creativeCommonsAttribution: LicenseTerms = {
transferable: true,
royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address
defaultMintingFee: 0n,
expiration: 0n,
commercialUse: true,
commercialAttribution: true,
commercializerChecker: zeroAddress,
commercializerCheckerData: "0x",
commercialRevShare: 0,
commercialRevCeiling: 0n,
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCelling: 0n,
currency: CURRENCY, // ex. $WIP address
uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/CC-BY.json",
};
```
* **Off-chain**
| Parameter | Options / Tags |
| --------------------------------- | --------------------------------------------------------------------------- |
| Territory | No restrictions |
| Channels of Distribution | No Restriction |
| Attribution | True |
| Content Standards | No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography |
| Sublicensable | False |
| AI Learning Models | True |
| Restriction on Cross-Platform Use | False |
| Governing Law | California, USA |
| Alternative Dispute Resolution | Tag: Alternative-Dispute-Resolution Ledger-Authoritative-Dispute-Resolution |
| Additional License Parameters | None |
# Examples
Here are some common examples of royalty flow. *More coming soon!*
## Example 1
### Explanation
Someone registers their Azuki on Story. By default, that IP Asset has Non-Commercial Social Remixing Terms, which specify that anyone can create derivatives of that work but cannot commercialize them. So, someone else creates & registers a remix of that work (IPA2) which inherits those same terms. Someone else then does the same to IPA2, creating & registering IPA3.
The owner of IPA1 then decides that others can commercialize the work, but they cannot create derivatives to do so, they must pay a 10 \$WIP minting fee, and they must share 10% of all revenue earned. So, someone wants to commercialize IPA1 by putting it on a t-shirt. They pay the 10 \$WIP minting fee to get a License Token, which represents the license to commercialize IPA1. They then put the image on a t-shirt and sell it. 10% of revenue earned by that t-shirt must be sent on-chain to IPA1.
## Example 2
### Explanation
Someone registers their Azuki on Story. By default, that IP Asset has Non-Commercial Social Remixing Terms, which specify that anyone can create derivatives of that work but cannot commercialize them. So, someone else creates & registers a remix of that work (IPA2) which inherits those same terms. Someone else then does the same to IPA2, creating & registering IPA3.
The owner of IPA1 then decides that others can create derivatives of their work and commercialize them, but they must pay a 10 \$WIP minting fee and share 10% of all revenue earned. So, someone wants to commercialize IPA1 by putting it on a t-shirt. They pay the 10 \$WIP minting fee to get a License Token and burn it to create their own derivative, which changes the background color to red. They then put the remixed image on a t-shirt and sell it. 10% of revenue earned by that t-shirt must be sent on-chain to IPA1.
A third person wants to commercialize the remix by putting it in a TV advertisement, but they want to change the hair color to white. So, they pay a 10 \$WIP minting fee (of which, 1 \$WIP gets sent back to IPA1) to create their own derivative. They then put the remixed image in a TV ad. 10% of TV advertising revenue earned must be sent on-chain to IPA4, of which 10% will be distributed back to IPA1.
# PIL Terms
Source: https://docs.story.foundation/concepts/programmable-ip-license/pil-terms
Detailed explanation of all terms available in the Programmable IP License
If you haven't already, read the Programmable IP License (PIL💊) overview.
Since there are so many possible combinations of the PIL, we have created
preset "flavors" for you to use while developing.
Check out the actual PIL legal text. It is very human-readable for a legal
text!
# On-chain terms
Most PIL terms are on-chain. They are implemented in the `IPILicenseTemplate.sol` contract as a `PILTerms` struct [here](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/interfaces/modules/licensing/IPILicenseTemplate.sol).
```solidity IPILicenseTemplate.sol theme={null}
/// @notice This struct defines the terms for a Programmable IP License (PIL).
/// These terms can be attached to IP Assets.
struct PILTerms {
bool transferable;
address royaltyPolicy;
uint256 defaultMintingFee;
uint256 expiration;
bool commercialUse;
bool commercialAttribution;
address commercializerChecker;
bytes commercializerCheckerData;
uint32 commercialRevShare;
uint256 commercialRevCeiling;
bool derivativesAllowed;
bool derivativesAttribution;
bool derivativesApproval;
bool derivativesReciprocal;
uint256 derivativeRevCeiling;
address currency;
string uri;
}
```
## Descriptions
| Parameter | Values | Description |
| --------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `transferable` | True/False | If false, the License Token cannot be transferred once it is minted to a recipient address. |
| `royaltyPolicy` | Address | The address of the royalty policy contract. |
| `defaultMintingFee` | # | The fee to be paid when minting a license. |
| `expiration` | # | The expiration period of the license. |
| `commercialUse` | True/False | You can make money from using the original IP Asset, subject to limitations below. |
| `commercialAttribution` | True/False | If true, people must give credit to the original work in their commercial application (eg. merch) |
| `commercializerChecker` | Address | Commercializers that are allowed to commercially exploit the original work. If zero address, then no restrictions are enforced. |
| `commercializerCheckerData` | Bytes | The data to be passed to the commercializer checker contract. |
| `commercialRevShare` | \[0-100,000,000] | Amount of revenue (from any source, original & derivative) that must be shared with the licensor (a value of 10,000,000 == 10% of revenue share). This will collect all revenue from tokens that are whitelisted in the [RoyaltyModule.sol contract](https://github.com/storyprotocol/protocol-core-v1/blob/e339f0671c9172a6699537285e32aa45d4c1b57b/contracts/modules/royalty/RoyaltyModule.sol#L50). |
| `commercialRevCeiling` | # | If `commercialUse` is set to true, this value determines the maximum revenue you can earn from the original work. |
| `derivativesAllowed` | True/False | Indicates whether the licensee can create derivatives of his work or not. |
| `derivativesAttribution` | True/False | If true, derivatives that are made must give credit to the original work. |
| `derivativesApproval` | True/False | If true, the licensor must approve derivatives of the work. |
| `derivativesReciprocal` | True/False | If false, you cannot create a derivative of a derivative. Set this to true to allow indefinite remixing. |
| `derivativeRevCeiling` | # | If `commercialUse` is set to true, this value determines the maximum revenue you can earn from derivative works. |
| `currency` | Address | The ERC20 token to be used to pay the minting fee. The token must be registered on Story. |
| `uri` | String | The URI of the license terms, which can be used to fetch [off-chain license terms](/concepts/programmable-ip-license/pil-terms#off-chain-terms-to-be-included-in-uri-field). |
# Off-chain terms to be included in `uri` field
Some PIL terms must be stored off-chain and passed in the `uri` field above. This is because these terms are often more lengthy and/or descriptive, so it would not make sense to store them on-chain.
| Parameter | Description |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `territory` | Limit usage of the IP to certain regions and/or countries. By default, the IP can be used globally. |
| `channelsOfDistribution` | Restrict usage of the IP to certain media formats and use in certain channels of distribution. By default, the IP can be used across all possible channels of distribution. Examples: "television", "physical consumer products", "video games", etc. |
| `attribution` | If the original author should be credited for usage of the IP. By default, you do not need to provide credit to the original author. |
| `contentStandards` | Set content standards around use of the IP. By default, no standards apply. Examples: "No-Hate", "Suitable-for-All-Ages", "No-Drugs-or-Weapons", "No-Pornography". |
| `sublicensable` | Derivative works can grant the same rights they received under this license to a 3rd party, without approval from the original licensor. By default, derivatives may not do so. |
| `aiLearningModels` | Whether or not the IP can be used to develop AI learning models. By default, the IP **cannot** be used for such development. |
| `restrictionOnCrossPlatformUse` | Limit licensing and creation of derivative works solely on the app on which the IP is made available. By default, the IP can be used anywhere. |
| `governingLaw` | The laws of a certain jurisdiction by which this license abides. By default, this is California, USA. |
| `alternativeDisputeResolution` | Please see section 3.1 (s) [here](https://github.com/piplabs/pil-document/blob/main/pil.pdf). |
| `PILUri` | The URI to the PIL legal terms. |
| `additionalParameters` | There may be other terms the licensor would like to add and they can do so in this tag. |
# Group IP Asset Registry
Source: https://docs.story.foundation/concepts/registry/group-ip-asset-registry
Learn about the registry responsible for managing Group IP Assets on Story.
View the smart contract for the Group IP Asset Registry.
The Group IP Asset Registry is responsible for managing the registration and tracking of Group IP Assets, including the group members and reward pools.
The Group IP Asset Registry will maintain grouping relationship on-chain between the Group's IP Account and individual IP Accounts through a mapping:
```solidity GroupIPAssetRegistry.sol theme={null}
mapping(address groupIpId => EnumerableSet.AddressSet memberIpIds) groups;
```
### Notable Functions
```solidity GroupIPAssetRegistry.sol theme={null}
function registerGroup(address groupNft, uint256 groupNftId, address rewardPool) external onlyGroupingModule whenNotPaused returns (address groupId)
```
This function registers a new Group IPA on Story.
```solidity GroupIPAssetRegistry.sol theme={null}
function addGroupMember(address groupId, address[] calldata ipIds) external onlyGroupingModule whenNotPaused
```
Adds already registered IPAs to an existing Group IPA.
```solidity GroupIPAssetRegistry.sol theme={null}
function removeGroupMember(address groupId, address[] calldata ipIds) external onlyGroupingModule whenNotPaused
```
Removes registered IPAs from a Group IPA.
# IP Asset Registry
Source: https://docs.story.foundation/concepts/registry/ip-asset-registry
Learn about the registry responsible for registering IPs on Story.
View the smart contract for the IP Asset Registry.
The IP Asset Registry is responsible for registering IPs into the protocol. It deploys a dedicated [IP Account](/concepts/ip-asset/ip-account) contract for each new IP Asset registered on the protocol (*NOTE: This registry inherits from the* [IP Account Registry](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/registries/IPAccountRegistry.sol))
### Notable Functions
```solidity IPAssetRegistry.sol theme={null}
function register(uint256 chainid, address tokenContract, uint256 tokenId) external whenNotPaused returns (address id)
```
This function registers an ERC-721 NFT as a new IP Asset on Story.
# License Registry
Source: https://docs.story.foundation/concepts/registry/license-registry
Learn about the registry that manages license-related states on Story.
View the smart contract for the License Registry.
The License Registry stores all license-related states within the protocol, including managing global state like registering new License Templates like the [Programmable IP License (PIL💊)](/concepts/programmable-ip-license/overview), attaching licenses to individual [IP Assets](/concepts/ip-asset/overview), registering derivatives, and the like:
```solidity LicenseRegistry.sol theme={null}
/// @dev Storage of the LicenseRegistry
/// @param defaultLicenseTemplate The default license template address
/// @param defaultLicenseTermsId The default license terms ID
/// @param registeredLicenseTemplates Registered license templates
/// @param registeredRoyaltyPolicies Registered royalty policies
/// @param registeredCurrencyTokens Registered currency tokens
/// @param parentIps Mapping of parent IPs to derivative IPs
/// @param parentLicenseTerms Mapping of parent IPs to license terms used to link to derivative IPs
/// @param childIps Mapping of derivative IPs to parent IPs
/// @param attachedLicenseTerms Mapping of attached license terms to IP IDs
/// @param licenseTemplates Mapping of license templates to IP IDs
/// @param expireTimes Mapping of IP IDs to expire times
/// @param licensingConfigs Mapping of minting license configs to a licenseTerms of an IP
/// @dev Storage structure for the LicenseRegistry
/// @custom:storage-location erc7201:story-protocol.LicenseRegistry
struct LicenseRegistryStorage {
address defaultLicenseTemplate;
uint256 defaultLicenseTermsId;
mapping(address licenseTemplate => bool isRegistered) registeredLicenseTemplates;
mapping(address childIpId => EnumerableSet.AddressSet parentIpIds) parentIps;
mapping(address childIpId => mapping(address parentIpId => uint256 licenseTermsId)) parentLicenseTerms;
mapping(address parentIpId => EnumerableSet.AddressSet childIpIds) childIps;
mapping(address ipId => EnumerableSet.UintSet licenseTermsIds) attachedLicenseTerms;
mapping(address ipId => address licenseTemplate) licenseTemplates;
mapping(bytes32 ipLicenseHash => Licensing.LicensingConfig licensingConfig) licensingConfigs;
}
```
### Notable Functions
```solidity LicenseRegistry.sol theme={null}
function attachLicenseTermsToIp(address ipId, address licenseTemplate, uint256 licenseTermsId) external onlyLicensingModule
```
This function allows you to attach License Terms to an IP Asset.
```solidity LicenseRegistry.sol theme={null}
function registerDerivativeIp(address childIpId, address[] calldata parentIpIds, address licenseTemplate, uint256[] calldata licenseTermsIds, bool isUsingLicenseToken) external onlyLicensingModule
```
This function allows you to register an IP Asset as a derivative of another IP Asset, unlocking things like claimable royalty flows from the [💸 Royalty Module](/concepts/royalty-module/overview).
# Module Registry
Source: https://docs.story.foundation/concepts/registry/module-registry
Learn about the registry that manages modules and hooks on Story.
View the smart contract for the Module Registry.
The Module Registry maintains and updates the global list of modules and hooks registered permissionlessly on Story. It can enable/disable modules on a per-IP Account basis for granular control over each IP Account's interaction with modules and hooks.
**This module is likely not very important for you** unless you wish to dive into creating/reading modules.
# 🗂️ Registry
Source: https://docs.story.foundation/concepts/registry/overview
Learn about the various registries that maintain the global state of Story's protocol.
The various registries on Story function as a primary directory/storage for the global states of the protocol. Obviously, they also contain functions to update that storage.
Unlike [⚙️ IP Accounts](/concepts/ip-asset/ip-account), which manage the state of specific IPs, a **registry** oversees the broader states of the protocol.
# Types of Registries
Below are all of the registries on Story.
## [IP Asset Registry](/concepts/registry/ip-asset-registry)
Responsible for registering IPs into the protocol.
## [Group IP Asset Registry](/concepts/registry/group-ip-asset-registry)
Responsible for registering and maintaining Group IP Assets.
## [License Registry](/concepts/registry/license-registry)
Stores all license-related states within the protocol, like attaching License Terms to IP Assets, registering derivatives, creating new License Templates, etc.
## [Module Registry](/concepts/registry/module-registry)
Maintains and updates the global list of modules and hooks registered permissionlessly on Story
# External Royalty Policies
Source: https://docs.story.foundation/concepts/royalty-module/external-royalty-policies
Learn about custom royalty policies that can be created for specific use cases
There can be many flavors and variations of royalty distribution rules as we observe in the real world. The same can be expected onchain. Whenever a use case requires unique and specific royalty rules, then those set of rules can be registered as an **External Royalty Policy**.
## 1. What is an External Royalty Policy?
It is a smart contract that inherits a specific interface called `IExternalRoyaltyPolicy`, which defines the view function below:
View the smart contract for external royalty policies.
```solidity IExternalRoyaltyPolicy.sol theme={null}
/// @notice Returns the amount of royalty tokens required to link a child to a given IP asset
/// @param ipId The ipId of the IP asset
/// @param licensePercent The percentage of the license
/// @return The amount of royalty tokens required to link a child to a given IP asset
function getPolicyRtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32);
```
After developing your smart contract make sure it inherits the interface above and you can register your new External Royalty Policy by calling `registerExternalRoyaltyPolicy` function in [RoyaltyModule.sol](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/modules/royalty/RoyaltyModule.sol).
## 2. How does it work?
Let's follow an example of a new External Royalty Policy called "Policy X".
### External Royalty Policies are selected by users
An IPA owner decides the royalty policy he/she wants to allow the IP to be remixed with. There are multiple options of royalty rules that can be chosen such as LAP, LRP and other External Royalty Policies. Let's say the user decides to mint a license token with "Policy X". After that, IP2 remixes IP1 and IP3 remixes IP2 and we have the situation as the image below:
Every time there is a remix - the link between the parent and derivative has 2 data points associated:
1. The royalty policy address
1. "Policy X" address in the example
2. The percentage of royalty tokens the parent demands from derivatives. This percentage can have different meanings depending on the royalty policy being used - ie. it can be a relative percentage, an absolute percentage, an adjusted percentage according to specific rules, etc.
1. 10% between IP1 and IP2
2. 50% between IP2 and IP3
### External Royalty Policies receive royalty tokens from their users' IPs
Following the example, when each remix is made and during the `onLinkToParents` function call in [RoyaltyModule.sol](https://github.com/storyprotocol/protocol-core-v1/blob/main/contracts/modules/royalty/RoyaltyModule.sol), the function `getPolicyRtsRequiredToLink` is called on the "Policy X" address.
```solidity IExternalRoyaltyPolic.sol theme={null}
/// @notice Returns the amount of royalty tokens required to link a child to a given IP asset
/// @param ipId The ipId of the IP asset
/// @param licensePercent The percentage of the license
/// @return The amount of royalty tokens required to link a child to a given IP asset
function getPolicyRtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32);
```
It should return the % of derivative's royalty tokens that the royalty policy demands for the link to happen. That share of royalty tokens are sent to the "Policy X" contract. In the example case:
* "Policy X" receives 3% of RT2 token supply that it can then redistributed to its userbase. IP1 owner wanted 10%, however - let's assume for the sake of the example - that due to the specific use case of "Policy X" and its custom logic, the IP2 owner is granted a special status in the platform in which it it has a 70% discount on the % share it has to give parent IPs due to having a very large distribution network to promote IPs. Therefore, instead of having to give 10% as the license percentage indicated it only gives 3%.
* "Policy X" receives 50% of RT3 token supply that it can then redistributed to its userbase.
### External Royalty Policies redistribute value back to their users according to custom rules
There are two ways in which an External Royalty Policy can redistribute value back to its users:
1. Send Royalty Tokens directly to its users
2. Keep the Royalty Tokens in the External Royalty Policy contract and have users claim Revenue Tokens through the said contract
Let's explore both in the context of "Policy X". Let's say that from the 50% of RT3 token supply "Policy X" received - 40% are kept in the "Policy X" contract and 10% are sent to an ancestor royalty vault (IP1).
Now let's imagine there is a 1M payment made to IP3 - an example of how the flow would be:
From the 1M WIP inflow to IP3 Royalty Vault:
* 500k WIP are claimed by the IP Account 3 which had 50% of RT3 token supply
* 100k WIP are claimed by the IP1 Royalty Vault which has 10% of RT3 token supply via `claimByTokenBatchAsSelf` function
* 400k WIP are claimed by "Policy X" which has 40 of RT3 token supply. This amount is further split by "Policy X" custom contract according to its specific rules - which define y% and z% - to its users.
# IP Royalty Vault
Source: https://docs.story.foundation/concepts/royalty-module/ip-royalty-vault
Learn about IP Royalty Vaults and how they manage revenue for IP Assets
An IP Royalty Vault is where all monetary inflows related to an IP Asset are stored. This revenue is then claimed with **Royalty Tokens**.
View the smart contract for the IP Royalty Vault.
## Royalty Tokens
When an IP Asset receives revenue, it is deposited into its IP Royalty Vault. In order to claim revenue from this vault, you must have the associated **Royalty Tokens**. Every vault has 100 Royalty Tokens associated with it. If an address owns these tokens, it is entitled to that % (% of the total supply of Royalty Tokens owned) of any future revenue in the IP Royalty Vault.
The IP Royalty Vault contract is also the ERC-20 contract for the **Royalty Tokens** of each IP Asset. This means the address of the IP Royalty Vault for an IP Asset is also the ERC-20 token address of the Royalty Tokens.
## Whitelisted Payment Tokens
An ERC-20 token must be whitelisted by our protocol in the [RoyaltyModule.sol contract](https://github.com/storyprotocol/protocol-core-v1/blob/e339f0671c9172a6699537285e32aa45d4c1b57b/contracts/modules/royalty/RoyaltyModule.sol#L50) to be used for making payments. Here are the whitelisted tokens:
| Token | Contract Address | Explorer | Mint |
| :----- | :------------------------------------------- | :--------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| WIP | `0x1514000000000000000000000000000000000000` | [View here ↗️](https://aeneid.storyscan.io/address/0x1514000000000000000000000000000000000000) | N/A |
| MERC20 | `0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E` | [View here ↗️](https://aeneid.storyscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E) | [Mint ↗️](https://aeneid.storyscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E?tab=write_contract#0x40c10f19) |
| Token | Contract Address | Explorer | Mint |
| :---- | :------------------------------------------- | :--------------------------------------------------------------------------------------------- | :--- |
| WIP | `0x1514000000000000000000000000000000000000` | [View here ↗️](https://aeneid.storyscan.io/address/0x1514000000000000000000000000000000000000) | N/A |
## How to obtain Royalty Tokens?
There are two ways that trigger the IP Royalty Vault deployment and make the initial Royalty Token distribution - whichever comes first:
1. A License Token is minted from an IP for the first time.
2. A derivative is registered under the IP.
In each case, the associated [IP Account](/concepts/ip-asset/ip-account), which is the IP itself, receives 100% of the Royalty Tokens.
Because Royalty Tokens are ERC-20, they can be transferred like any other token. Thus, the IP Account could send them to someone else, or even put them up for sale on the secondary market. Once they are transferred, the new owner now has the right to claim x% of the revenue from the royalty vault.
## Examples
Let's look at some examples of royalty flow from the IP Royalty Vault to Royalty Token holders.
### Default Case
By default, [when the royalty vault is deployed](/concepts/royalty-module/ip-royalty-vault#how-to-obtain-royalty-tokens%3F), the IP Account (which is the IP itself) receives 100% of the Royalty Tokens.
Let's look at an example where the IP Account is the full owner of the Royalty Tokens, and a payment is received into the IP Royalty Vault.
As you can see, each IP claims the full amount from its royalty vault because it has 100% of the royalty tokens.
### Multiple Royalty Token Destinations
Now let's look at a case where the IP Asset 2 has distributed 5% of its Royalty Tokens to another address. Maybe this is because they sold them on the secondary market, or maybe it's because they gave them to a friend.
As you can see, IP Asset 1 claims its full 10 \$WIP. But IP Asset 2 only claims 9.5 \$WIP. The other 0.5 \$WIP is claimed by the address that owns the 5% of the Royalty Tokens.
## Distributing Royalty Tokens
One of the most common questions is: *"Why does royalty end up in the IP Account? Shouldn't it end up in the IP owner's wallet?"*
While the IP Account is the initial owner of 100% of the Royalty Tokens, remember that as the IP owner, you can transfer them to whoever. An easy solution is to transfer 100% of the Royalty Tokens to the IP owner's wallet, so that when revenue is received to the IP Royalty Vault, revenue can be claimed directly to the wallet.
View a working code example of transferring Royalty Tokens to an external
wallet (like the IP owner's wallet). Remember, Royalty Tokens are simple
ERC-20s!
# Liquid Absolute Percentage (LAP)
Source: https://docs.story.foundation/concepts/royalty-module/liquid-absolute-percentage
Learn how the Liquid Absolute Percentage royalty policy works.
The Liquid Absolute Percentage (LAP) defines that each parent IP Asset can choose a minimum royalty percentage that **all** of its downstream IP Assets in a derivative chain will share from their monetary gains as defined in the license agreement.
View the smart contract for the LAP Royalty Policy.
## Calculated Royalty Stack
In the image below, IPA 1 and IPA 2 - due to being ancestors of IPA 3 - have a % economic right over revenue made by IPA 3. Key notes to understand the derivative chain below:
* **License Royalty %**: this percentage is selected by the user and it means the percentage that the user wants - according to LAP rules - in return for allowing other users to remix its IPA.
* **Royalty Stack**: the total revenue % an IPA has to pay to all its ancestors. For LAP, it's the sum of parents royalty stack + sum of licenses percentages used to connect to each parent
* Royalty Stack IPA 2 = Royalty Stack IPA 1 + License Royalty % between IPAs 1 and 2 = 0% + 5% = 5%
* Royalty Stack IPA 3 = Royalty Stack IPA 2 + License Royalty % between IPAs 2 and 3 = 5% + 10% = 15%
The "License Royalty %" in this diagram corresponds to the same value as the `commercialRevShare` on the [PIL terms](/concepts/programmable-ip-license/pil-terms).
## Royalty Module Split
Let's show an example where a payment is made to IPA 3. In the below diagram, you can see that initial payment is forwarded to the Royalty Module. The Royalty Module then splits the payment based on the **Royalty Stack** determined by the LAP policy:
* 15 \$WIP is sent to the LAP policy contract because of the 15% LAP royalty stack
* 85 \$WIP is sent to the IPA 3 vault
## Royalty Policy LAP Distribution
After this initial payment is complete, IPA 1 and 2 can claim their revenue from the LAP policy contract. The LAP policy contract will then distribute the revenue to the parents based on the negotiatedrevenue share percentages:
* IPA2 gets 10% absolute percentage of IPA3's revenue (10 \$WIP)
* IPA1 gets 5% absolute percentage of IPA3's revenue (5 \$WIP)
# Liquid Relative Percentage (LRP)
Source: https://docs.story.foundation/concepts/royalty-module/liquid-relative-percentage
Learn how the Liquid Relative Percentage royalty policy works.
The Liquid Relative Percentage (LRP) royalty policy defines that each parent IP Asset can choose a minimum royalty percentage that **only the direct derivative IP Assets in a derivative chain** will share from their monetary gains as defined in the license agreement.
View the smart contract for the LRP Royalty Policy.
## Calculated Royalty Stack
In the image below, IPA 1 and IPA 2 - due to being ancestors of IPA 3 - have a % economic right over revenue made by IPA 3. Key notes to understand the derivative chain below:
* **License Royalty %**: this percentage is selected by the user and it means the percentage that the user wants - according to LRP rules - in return for allowing other users to remix its IPA.
* **Royalty Stack**: the total revenue % an IPA has to pay to all its parents. For LRP, it's the sum of license percentages used to connect to each parent
* Royalty Stack IPA 2 = License Royalty % between IPAs 1 and 2 = 5%
* Royalty Stack IPA 3 = License Royalty % between IPAs 2 and 3 = 10%
The "License Royalty %" in this diagram corresponds to the same value as the `commercialRevShare` on the [PIL terms](/concepts/programmable-ip-license/pil-terms).
## Royalty Module Split
Let's show an example where a payment is made to IPA 3. In the below diagram, you can see that initial payment is forwarded to the Royalty Module. The Royalty Module then splits the payment based on the **Royalty Stack** determined by the LRP policy:
* 10 \$WIP is sent to the LRP policy contract because of the 10% LRP royalty stack
* 90 \$WIP is sent to the IPA 3 vault
## Royalty Policy LRP Distribution
After this initial payment is complete, IPA 1 and 2 can claim their revenue from the LRP policy contract. The LRP policy contract will then distribute the revenue to the parents based on the negotiated revenue share percentages:
* IPA2 gets 10% of IPA3's revenue (10 \$WIP)
* IPA1 gets 5% of IPA2's revenue (0.5 \$WIP)
* IPA2 is left with 9.5 \$WIP
# 💸 Royalty Module
Source: https://docs.story.foundation/concepts/royalty-module/overview
Learn how to pay, route, and claim royalties on Story.
Story's Royalty Module enables automated revenue sharing between [IP Assets (IPAs)](/concepts/ip-asset/overview) based on their derivative relationships and license terms. This document explains how revenue flows through the protocol and how IP owners are paid and can claim their share.
A working code example using the TypeScript SDK that shows how to pay and claim royalties.
View the smart contract for the Royalty Module.
# Conceptual Overview
In order to learn the Royalty Module, let's look at a full example just so you can see what it looks like. Then we will walk through each part step-by-step to get a comprehensive understanding.
## The Ancestory Graph
IP Assets are connected by their derivative relationships. These relationships are bound via [License Terms](/concepts/licensing-module/license-terms), which specify things like an initial minting fee or a royalty percentage that must be paid every time revenue is generated.
In this example, let's say we have 3 IP Assets with the following configurations:
| IP Asset | Relationship | Commercial Revenue Share |
| ----------------- | -------------------- | --------------------------------- |
| IP Asset 1 (IPA1) | Original IP Asset | Requires 5% from all derivatives |
| IP Asset 2 (IPA2) | A derivative of IPA1 | Requires 10% from all derivatives |
| IP Asset 3 (IPA3) | A derivative of IPA2 | |
### Royalty Stack
Each IP Asset has what's called a **Royalty Stack**. This is a cumulative percentage of revenue that must be paid to ancestors based on the license terms between the IP Assets.
For example, if IPA3 makes 100 \$IP, it will eventually only take in 85 \$IP because 15 \$IP will be paid to its ancestors. This is because the royalty stack is 15% (5% from IPA1 and 10% from IPA2).
## IP Royalty Vault
Upon creation, every IP Asset automatically gets a **Royalty Vault**. This is a vault - separate from the actual IP itself but bound to it - that receives all incoming revenue generated by the IP Asset.
### Royalty Tokens
This royalty vault has 100 **Royalty Tokens** associated with it, where each token represents the right to 1% of the total revenue generated by the IP Asset and thus deposited to the Royalty Vault.
When an IP Asset is created and a Royalty Vault is deployed, it automatically sends the 100 Royalty Tokens to the IP Asset itself.
These Royalty Tokens can be sent out to other wallets by the IP owner. Then, as stated above, whoever owns them has the right to claim their % due share of the revenue that gets accepted into the Royalty Vault.
This unlocks some really cool DeFi opportunities. For example, selling Royalty Tokens on the secondary market where people can buy them and then claim their share of the revenue when it gets deposited into the Royalty Vault.
## Whitelisted Payment Tokens
For a currency to be used in Story's Royalty Module, it must be whitelisted by our protocol in the [RoyaltyModule.sol contract](https://github.com/storyprotocol/protocol-core-v1/blob/e339f0671c9172a6699537285e32aa45d4c1b57b/contracts/modules/royalty/RoyaltyModule.sol#L50). Here are the whitelisted tokens:
| Token | Contract Address | Explorer | Mint |
| :----- | :------------------------------------------- | :--------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| WIP | `0x1514000000000000000000000000000000000000` | [View here ↗️](https://aeneid.storyscan.io/address/0x1514000000000000000000000000000000000000) | N/A |
| MERC20 | `0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E` | [View here ↗️](https://aeneid.storyscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E) | [Mint ↗️](https://aeneid.storyscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E?tab=write_contract#0x40c10f19) |
| Token | Contract Address | Explorer | Mint |
| :---- | :------------------------------------------- | :--------------------------------------------------------------------------------------------- | :--- |
| WIP | `0x1514000000000000000000000000000000000000` | [View here ↗️](https://aeneid.storyscan.io/address/0x1514000000000000000000000000000000000000) | N/A |
## Initiating a Payment
There are two common scenarios when revenue flow would be initiated:
| Payment Type | When It Happens | Potential Functions Called |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| Minting a License | When someone mints a license to use your IP, or registers a derivative of an IP Asset (which requires having a license) | `mintLicenseTokens`, `registerDerivative` |
| Paying Revenue Directly | When someone directly sends revenue to an IP Asset either as a tip or to forward due revenue to the chain. | `payRoyaltyOnBehalf` |
Let's see what happens when IP Asset 3 earns money using the [payRoyaltyOnBehalf](/sdk-reference/royalty#payroyaltyonbehalf) function.
In our example, let's assume that IP Asset 3 is a video game character that is earning 100 \$IP. This payment is made via `payRoyaltyOnBehalf`, which will initiate the payment process as seen below.
Next, the Royalty Module will split the payment based on the **royalty stack** (15%) and distribute the funds to both the IP Asset and the Royalty Policy contract which governs the license terms. Royalty policies will be explained in a different section.
A tutorial on how to pay an IP revenue using the TypeScript SDK.
A tutorial on how to pay an IP revenue using the smart contracts.
## Claiming Revenue
The above walked through what happens when a payment is made. However, in order to actually get the revenue, the revenue must be claimed to whoever holds the Royalty Tokens.
Imagine an IP graph with thousands of ancestors. If we were to pay, distribute, and claim all at once, the transaction would be too large to handle. By separating the payment and claiming processes, we can break down the process into smaller chunks that are more gas efficient.
Claiming revenue can be done with the [claimAllRevenue](/sdk-reference/royalty#claimallrevenue) function. Let's see how it works.
Claiming revenue is completely permissionless. Anyone can claim revenue on behalf of any IP Asset.
Since IP Asset 3 was paid directly, the revenue automatically went to it's Royalty Vault. However because IP Asset 1 and 2 earned their revenue from a derivative relationship, the revenue must be transferred from the royalty policy to their Royalty Vaults.
In the last step, revenue will be transferred from the Royalty Vault to whoever owns the Royalty Tokens for each associated IP Asset. This example is simple, because each IP Asset is holding all 100 of its Royalty Tokens in the IP itself.
A tutorial on how to claim revenue using the TypeScript SDK.
A tutorial on how to claim revenue using the smart contracts.
# Batch Function Calls
Source: https://docs.story.foundation/concepts/spg/batch-spg-function-calls
Learn how to batch multiple operations into a single transaction for efficiency
## Background
Prior to this point, registering multiple IPs or performing other operations such as minting, attaching licensing terms, and registering derivatives requires separate transactions for each operation. This can be inefficient and costly. To streamline the process, you can batch multiple transactions into a single one. Two solutions are now available for this:
1. **Batch SPG function calls:** Use [SPG's built-in `multicall` function](#1-batch-spg-function-calls-via-built-in-multicall-function).
2. **Batch function calls beyond SPG:** Use the [Multicall3 Contract](#2-batch-function-calls-via-multicall3-contract).
***
## 1. Batch SPG Function Calls via Built-in `multicall` Function
SPG includes a `multicall` function that allows you to combine multiple read or write operations into a single transaction.
### Function Definition
The `multicall` function accepts an array of encoded call data and returns an array of encoded results corresponding to each function call:
```solidity Solidity theme={null}
/// @dev Executes a batch of function calls on this contract.
function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results);
```
### Example Usage
Suppose you want to mint multiple NFTs, register them as IPs, and link them as derivatives to some parent IPs.
To accomplish this, you can use SPG's `multicall` function to batch the calls to the `mintAndRegisterIpAndMakeDerivative` function.
Here's how you might do it:
```solidity Solidity theme={null}
// an SPG workflow contract: https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/workflows/DerivativeWorkflows.sol
contract DerivativeWorkflows {
...
function mintAndRegisterIpAndMakeDerivative(
address nftContract,
MakeDerivative calldata derivData,
IPMetadata calldata ipMetadata,
address recipient
) external returns (address ipId, uint256 tokenId) {
....
}
...
}
```
To batch call `mintAndRegisterIpAndMakeDerivative` using the `multicall` function:
```javascript JavaScript theme={null}
// batch mint, register, and make derivatives for multiple IPs
await DerivativeWorkflows.multicall([
DerivativeWorkflows.contract.methods.mintAndRegisterIpAndMakeDerivative(
nftContract1,
derivData1,
recipient1,
ipMetadata1,
).encodeABI(),
DerivativeWorkflows.contract.methods.mintAndRegisterIpAndMakeDerivative(
nftContract2,
derivData2,
recipient2,
ipMetadata2,
).encodeABI(),
DerivativeWorkflows.contract.methods.mintAndRegisterIpAndMakeDerivative(
nftContract3,
derivData3,
recipient3,
ipMetadata3,
).encodeABI(),
...
// Add more calls as needed
]);
```
***
## 2. Batch Function Calls via Multicall3 Contract
The Multicall3 contract is not fully compatible with SPG functions that involve SPGNFT minting due to access control and context changes during Multicall execution. For such operations, use [SPG's built-in multicall function.](#1-batch-spg-function-calls-via-built-in-multicall-function)
The Multicall3 contract allows you to execute multiple calls within a single transaction and aggregate the results. The [`viem` library](https://viem.sh/docs/contract/multicall#multicall) provides native support for Multicall3.
### Story Aeneid Testnet Multicall3 Deployment Info
(Same address across all EVM chains)
```json theme={null}
{
"contractName": "Multicall3",
"chainId": 1516,
"contractAddress": "0xcA11bde05977b3631167028862bE2a173976CA11",
"url": "https://aeneid.storyscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11"
}
```
### Main Functions
To batch multiple function calls, you can use the following functions:
1. **`aggregate3`**: Batches calls using the `Call3` struct.
2. **`aggregate3Value`**: Similar to `aggregate3`, but also allows attaching a value to each call.
```solidity Solidity theme={null}
/// @notice Aggregate calls, ensuring each returns success if required.
/// @param calls An array of Call3 structs.
/// @return returnData An array of Result structs.
function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData);
/// @notice Aggregate calls with an attached msg value.
/// @param calls An array of Call3Value structs.
/// @return returnData An array of Result structs.
function aggregate3Value(Call3Value[] calldata calls) external payable returns (Result[] memory returnData);
```
### Struct Definitions
* **Call3**: Used in `aggregate3`.
* **Call3Value**: Used in `aggregate3Value`.
```solidity Solidity theme={null}
struct Call3 {
address target; // Target contract to call.
bool allowFailure; // If false, the multicall will revert if this call fails.
bytes callData; // Data to call on the target contract.
}
struct Call3Value {
address target;
bool allowFailure;
uint256 value; // Value (in wei) to send with the call.
bytes callData; // Data to call on the target contract.
}
```
### Return Type
* **Result**: Struct returned by both `aggregate3` and `aggregate3Value`.
```solidity Solidity theme={null}
struct Result {
bool success; // Whether the function call succeeded.
bytes returnData; // Data returned from the function call.
}
```
For detailed examples in Solidity, TypeScript, and Python, see the [Multicall3 repository](https://github.com/mds1/multicall/tree/main/examples).
### Limitations
For a list of limitations when using Multicall3, refer to the [Multicall3 README](https://github.com/mds1/multicall/blob/main/README.md#batch-contract-writes).
### Additional Resources
* [Multicall3 Documentation](https://github.com/mds1/multicall/blob/main/README.md)
* [Multicall Documentation from Viem](https://viem.sh/docs/contract/multicall#multicall)
### Full Multicall3 Interface
```solidity Solidity theme={null}
interface IMulticall3 {
struct Call {
address target;
bytes callData;
}
struct Call3 {
address target;
bool allowFailure;
bytes callData;
}
struct Call3Value {
address target;
bool allowFailure;
uint256 value;
bytes callData;
}
struct Result {
bool success;
bytes returnData;
}
function aggregate(Call[] calldata calls) external payable returns (uint256 blockNumber, bytes[] memory returnData);
function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData);
function aggregate3Value(Call3Value[] calldata calls) external payable returns (Result[] memory returnData);
function blockAndAggregate(Call[] calldata calls) external payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData);
function getBasefee() external view returns (uint256 basefee);
function getBlockHash(uint256 blockNumber) external view returns (bytes32 blockHash);
function getBlockNumber() external view returns (uint256 blockNumber);
function getChainId() external view returns (uint256 chainId);
function getCurrentBlockCoinbase() external view returns (address coinbase);
function getCurrentBlockDifficulty() external view returns (uint256 difficulty);
function getCurrentBlockGasLimit() external view returns (uint256 gaslimit);
function getCurrentBlockTimestamp() external view returns (uint256 timestamp);
function getEthBalance(address addr) external view returns (uint256 balance);
function getLastBlockHash() external view returns (bytes32 blockHash);
function tryAggregate(bool requireSuccess, Call[] calldata calls) external payable returns (Result[] memory returnData);
function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) external payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData);
}
```
# 📦 SPG (Periphery)
Source: https://docs.story.foundation/concepts/spg/overview
Learn about the Story Protocol Gateway that simplifies interactions with the protocol
The Story Protocol Gateway (SPG) is a group of periphery/utility smart contracts, deployed on our protocol that **allows you to combine independent operations** - like registering an [🧩 IP Asset](/concepts/ip-asset/overview) and attaching License Terms to that IP Asset - **into one transaction to make your life easier**.
This was primarily developed to make our [SDK](/sdk-reference) easier to use.
For example, this `mintAndRegisterIpAndAttachPILTerms` is one of the functions in the SPG (more specifically in the `LicenseAttachmentWorkflows.sol`) that allows you to mint an NFT, register it as an IP Asset, and attach License Terms to it all in one call:
```solidity LicenseAttachmentWorkflows.sol theme={null}
function mintAndRegisterIpAndAttachPILTerms(
address spgNftContract,
address recipient,
WorkflowStructs.IPMetadata calldata ipMetadata,
WorkflowStructs.LicenseTermsData[] calldata licenseTermsData,
bool allowDuplicates
) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds)
```
## All Supported Workflows
As mentioned above, there are many different functions we have created for you that combine multiple functions into one. We have categorized them into different groups. These groups are called "workflows".
Click here to view all of the supported workflows.
Click here to view the workflow smart contracts.
## Batching Calls
Although the SPG contains certain functions like `mintAndRegisterIpAndAttachPILTerms`, `registerIpAndAttachPILTerms`, and a bunch more, it would be tedious for us to continually update the contract to account for every single combination of possible interactions with an IP Asset.
Instead, we have allowed for a "Multicall" mechanism where you can batch transactions how you like. For more info, see [Batch Function Calls](/concepts/spg/batch-spg-function-calls).
# Story Attestation Service
Source: https://docs.story.foundation/concepts/story-attestation-service
A multi-layered decentralized approach to validating intellectual property.
You can think of the Story Attestation Service (SAS) as a bunch of independent service providers each proving the validity of an IP in their own way. So that each IP has a set of "badges" on it displaying the results.
It's then up to the ecosystem/market to determine which providers they trust or want to believe. This becomes a decentralized "validator"-like approach to IP validity, where if an IP Asset has lots of providers saying it is valid, then it's probably valid.
Story employs a multi-layered decentralized approach to validating intellectual property, grounded in two foundational components:
1. The Story Attestation Service (SAS): leverages a network of specialized service providers — each detecting copyright violations across different mediums (images, audio, etc) — to provide transparent, publicly accessible signals on the legitimacy of an [🧩 IP Asset](/concepts/ip-asset). Applications that facilitate IP registration (e.g. original content) may also attest to the provenance of an IP asset (called "apptestations") in the future.
2. The [❌ Dispute Module](/concepts/dispute-module): offers a flexible framework for resolving conflicts, tapping both on-chain and off-chain processes to accommodate the nuanced nature of IP disputes.
This blend of detection methods and dispute resolution creates a robust ecosystem that allows IP to be registered without introducing undue friction, while letting the market, and individual ecosystem apps, determine how much weight to give each attestation provider.
These layers make up the **IP Validation Service (IPVS)** - a fully decentralized marketplace of trust. The existing system of detection providers will continue to expand into a broader ecosystem of signal contributors, each able to offer specialized, verifiable assessments of IP authenticity. Through incentivized participation, IPVS fosters a self-sustaining market where different validators collaborate to deliver specialized signals.
So rather than preventing duplicates, which would cause far more potentially disruptive front running risk, the signals and attestations allow the original IPs surface above the rest.
## "When does the SAS scan my IP?"
It's important to note that the Story Attestation Service only runs IP infringement checks on **commercial IP**. That is, IP Assets who have at least one [License Terms](/concepts/licensing-module/license-terms) where `commercialUse = true`.
If your IP is non-commercial, then this section doesn't apply to you.
When [registering your IP on Story](/developers/tutorials/how-to-register-ip-on-story), you pass in IP-specific metadata that implements the [📝 IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard). In this standard, you'll see 3 fields:
| Property Name | Type | Description |
| :------------ | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mediaUrl` | `string` | Used for infringement checking, points to the actual media (ex. image or audio) |
| `mediaHash` | `string` | Hashed string of the media using SHA-256 hashing algorithm. See [here](/concepts/ip-asset/ipa-metadata-standard#hashing-content) for how that is done. |
| `mediaType` | `string` | Type of media (audio, video, image), based on [mimeType](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types). See the allowed media types [here](/concepts/ip-asset/ipa-metadata-standard#media-types). |
These are used for the commercial infringement check. Whatever media you pass in through `mediaUrl` will be checked by our infringement detection providers and flagged if infringement is detected.
If you do not pass in these `media.*` fields, then an infringement detection will not be performed and your IP will not be proven valid.
### Current Limitations
* You must set the `media.*` fields before attaching commercial terms (`commercialUse = true`), otherwise no check will be performed.
* Attestations will only show up on the IP Portal (our "GitHub for IP" platform coming soon). We are working on publishing attestations to public record so anyone can access the results (**COMING SOON!**).
* Only media that is **existing on the internet** will be detected. If someone registers new IP on Story, it will simply return validated because our providers don't have data on it.
## Current Providers
Yakoa uses AI and machine learning to scan multiple blockchains, analyzing on-chain data to detect direct copies, stylistic forgeries, and unauthorized replications of digital assets. It compares new assets against a database of known IP, flagging potential violations in real time and providing detailed audit logs for enforcement.
Pex.com is a digital platform that leverages advanced content recognition and analytics to help creators and rights holders track, manage, and monetize their visual and audio media online. It monitors how content is used across the web, making it easier for users to discover licensing opportunities and protect their intellectual property.
## Becoming an Attestation Provider
The Story Attestation Service is undergoing active development. If you run any form of IP validation (infringement, identity, origin, etc), then you can become an attestation provider. To do so, please fill out this [form](https://docs.google.com/forms/d/10n3AnWoiLsxpaY17kJlxRazysDe8aOWJgirRnfkFRAk/edit).
# Demo
Source: https://docs.story.foundation/demo
# Runtime Configuration
Source: https://docs.story.foundation/developers/cdr-sdk/advanced-configuration
Story-API endpoint, DKG state, threshold tuning, system addresses, and Aeneid runtime notes for the CDR SDK.
This page covers the release-specific and operational details that sit beyond
basic setup. Start with [Setup CDR Client](/developers/cdr-sdk/setup) first.
## DKG State and the Story-API Endpoint
The SDK reads state from two backends:
| Backend | Configured by | What it reads |
| ------------------ | -------------- | -------------------------------------------------------------------------------- |
| **EVM** | `publicClient` | CDR contract state — vaults, fees, `maxEncryptedDataSize`, operational threshold |
| **Story-API REST** | `apiUrl` | DKG state — active round, global public key, threshold, validators, attestations |
The `apiUrl` is a **required** `CDRClient` parameter. It is the base URL of a
Story-API REST endpoint, and the `observer` uses it for every DKG read.
```typescript theme={null}
const client = new CDRClient({
network: "testnet",
publicClient,
walletClient,
apiUrl: "http://172.192.41.96:1317",
});
```
For production deployments, point `apiUrl` at your own Story node's REST
gateway rather than the shared endpoint. See
[Story-API REST Endpoint](/developers/cdr-sdk/setup#story-api-rest-endpoint)
for the per-network values.
The `observer` caches round-keyed DKG snapshots for rounds in the stable
Active and Ended stages, with in-flight request deduplication. The active
round itself is always re-fetched, since it can transition at any time.
## Threshold Tuning
By default the SDK uses the network's own DKG threshold when combining partial
decryptions. You can raise the bar with the optional `minThresholdRatio`
parameter, a value in `[0, 1]`:
```typescript theme={null}
const client = new CDRClient({
network: "testnet",
publicClient,
walletClient,
apiUrl: "http://172.192.41.96:1317",
minThresholdRatio: 0.67,
});
```
The effective threshold becomes
`max(network.threshold, ceil(participants * minThresholdRatio))`.
Values above `1` would require more partials than there are participants,
causing `collectPartials` / `accessCDR` to time out forever. The SDK rejects
them.
## Contract Addresses
The current Aeneid release uses the following core system addresses:
| Contract | Address |
| -------- | -------------------------------------------- |
| DKG | `0xCcCcCC0000000000000000000000000000000004` |
| CDR | `0xCcCcCC0000000000000000000000000000000005` |
The SDK already knows these addresses. For Story license-gated condition
contracts, see
[How the Story License Read Pattern Works](/developers/cdr-sdk/ip-asset-vaults#how-the-story-license-read-pattern-works).
## Release Posture and Availability
* Aeneid is the current public **testnet** release for the SDK.
* Confidentiality depends on the DKG threshold and validator / enclave trust
assumptions described in the CDR overview.
* Availability depends on enough validators responding before timeout. If reads
fail to reach threshold, retry or increase `timeoutMs`.
* DKG reads depend on the `apiUrl` Story-API endpoint being correct and
reachable. Point it at a node you trust for production use.
## Current Release Notes
* Story license-gated vaults still require manual condition encoding.
* The CDR SDK does not auto-wrap IP to WIP or auto-approve WIP for license
minting.
* `accessCDR()` can auto-generate the ephemeral keypair and query `threshold`
when those params are omitted.
* `createVault` / `readVault` / `createFileVault` / `readFileVault` are
available as high-level aliases.
* `getRegisteredValidators()` only includes validators whose registration is
fully ratified (`status = Finalized`).
* `timeoutMs: 120_000` is a good starting point for `accessCDR()` and
`downloadFile()`.
# Encrypt & Decrypt
Source: https://docs.story.foundation/developers/cdr-sdk/encrypt-and-decrypt
Learn how to encrypt a secret and decrypt it using CDR threshold decryption.
This guide walks through the two main CDR flows:
* `uploadCDR` / `accessCDR` for small secrets stored directly on-chain
* `uploadFile` / `downloadFile` for larger encrypted files stored off-chain
### Prerequisites
* [CDR SDK setup](/developers/cdr-sdk/setup) complete with WASM initialized and client created
## What Runs On-Chain vs Off-Chain
| Operation | Sends transaction? | What happens |
| ---------------------------- | ---------------------------- | -------------------------------------------------------------------------- |
| `observer.getGlobalPubKey()` | No | Pure read of DKG state over the Story-API REST endpoint |
| `uploadCDR()` | Yes, 2 txs | Local TDH2 encryption plus `allocate()` and `write()` |
| `uploadFile()` | Yes, 2 txs + storage upload | Local AES encryption, storage upload, then `allocate()` and `write()` |
| `accessCDR()` | Yes, 1 tx | `read()` on-chain, then off-chain partial collection and local combination |
| `downloadFile()` | Yes, 1 tx + storage download | `accessCDR()` plus encrypted file download and local AES decryption |
## Encrypt a Secret
The diagram below shows the on-chain secret flow: allocate a vault, encrypt the
secret locally with TDH2, and write the ciphertext to the vault.
The simplest "owner-only" pattern uses your wallet (EOA) address as both the
write and read condition. The CDR contract bypasses the condition check when
`msg.sender` equals the configured condition address, so only that wallet can
write or read the vault. Because the high-level `uploadCDR()` helper validates
that condition addresses point at deployed condition contracts, EOA conditions
are configured through the low-level `allocate()` call with
`skipConditionValidation: true`.
```typescript theme={null}
import { initWasm, uuidToLabel } from "@piplabs/cdr-sdk";
import { toHex } from "viem";
await initWasm();
// Assumes `client` and `walletClient` are already created (see Setup)
const { uploader, observer } = client;
const walletAddress = walletClient.account!.address;
// Pure read: fetch the DKG global public key
const globalPubKey = await observer.getGlobalPubKey();
// Encode your secret as bytes
const secret = "my confidential data";
const dataKey = new TextEncoder().encode(secret);
// On-chain transaction: allocate a vault using the wallet address as the
// write AND read condition. Only this EOA can write or read.
const { uuid, txHash: allocateTx } = await uploader.allocate({
updatable: false,
writeConditionAddr: walletAddress,
readConditionAddr: walletAddress,
writeConditionData: "0x",
readConditionData: "0x",
skipConditionValidation: true,
});
// Local: TDH2-encrypt the secret, bound to this vault's UUID
const label = uuidToLabel(uuid);
const ciphertext = await uploader.encryptDataKey({
dataKey,
globalPubKey,
label,
});
// On-chain transaction: write encrypted data to the vault
const { txHash: writeTx } = await uploader.write({
uuid,
accessAuxData: "0x",
encryptedData: toHex(ciphertext.raw),
});
console.log(`Vault created with UUID: ${uuid}`);
console.log(`Allocate tx: ${allocateTx}`);
console.log(`Write tx: ${writeTx}`);
```
Any EOA address works as a write or read condition — only that EOA can
perform the matching action. To gate just one side, set your wallet address
on that side and a condition contract (such as `LicenseReadCondition`) on the
other. The high-level `uploadCDR()` helper expects deployed condition
contracts on both sides and does not support EOA conditions, so use it for
patterns like Story license-gated reads (see [IP Asset
Vaults](/developers/cdr-sdk/ip-asset-vaults)) and use the low-level
`allocate()` + `write()` flow above for owner-only EOA conditions.
The value of the transaction must be exactly the same as the fee.
`dataKey` is the historical parameter name. In `encryptDataKey()` it can be
any secret bytes, not just a cryptographic key.
Vault encrypted data is limited to **1024 bytes** on Aeneid
(`maxEncryptedDataSize`). TDH2 adds overhead, so the maximum plaintext is
smaller. For larger content, use `uploadFile()` so only a small `{cid, key}`
payload is written to the vault.
## Decrypt a Secret
Decryption requires submitting a read request on-chain, collecting partial
decryptions from validators, and combining them client-side.
```typescript theme={null}
const { consumer } = client;
// Sends 1 transaction, then collects partials and combines them locally
const { dataKey, txHash } = await consumer.accessCDR({
uuid,
accessAuxData: "0x",
timeoutMs: 120_000, // wait up to 2 minutes for validators
});
const secret = new TextDecoder().decode(dataKey);
console.log(`Read tx: ${txHash}`);
console.log(`Decrypted secret: ${secret}`);
```
`accessCDR()` auto-generates the ephemeral keypair and auto-queries
`globalPubKey` when you omit them. The threshold is derived automatically
from the partial-decryption bucket's DKG round.
The timeout of the request on the server side is 200 blocks, which is
approximately 7 minutes. If you're not able to collect enough partials within
this timeout, try another read request.
```typescript theme={null}
import { secp256k1 } from "@noble/curves/secp256k1";
import { toHex } from "viem";
const { consumer, observer } = client;
const globalPubKey = await observer.getGlobalPubKey();
const recipientPrivKey = secp256k1.utils.randomPrivateKey();
const requesterPubKey = toHex(
secp256k1.getPublicKey(recipientPrivKey, false),
);
const { dataKey, txHash } = await consumer.accessCDR({
uuid,
accessAuxData: "0x",
requesterPubKey,
recipientPrivKey,
globalPubKey,
timeoutMs: 120_000,
});
console.log(`Read tx: ${txHash}`);
console.log(new TextDecoder().decode(dataKey));
```
## Encrypt and Download a File
Use the file workflow when the encrypted payload should live off-chain and only
the encrypted file key plus pointer should be stored in the vault.
Upload happens once by the data owner. Download happens later by an authorized
reader who recovers the vault payload and then decrypts the stored file.
The `uploadFile()` helper requires deployed condition contracts on both sides,
so the example below uses Story's `OwnerWriteCondition` for the write side and
`LicenseReadCondition` for the read side. License token holders can decrypt
the file (see [IP Asset Vaults](/developers/cdr-sdk/ip-asset-vaults) for the
end-to-end license setup). For an owner-only file flow, replicate the
low-level steps shown earlier with your wallet (EOA) address as both
conditions.
```typescript theme={null}
import { HeliaProvider } from "@piplabs/cdr-sdk";
import { readFile, writeFile } from "node:fs/promises";
import { createHelia } from "helia";
import { unixfs } from "@helia/unixfs";
import { CID } from "multiformats/cid";
import { encodeAbiParameters } from "viem";
const uploaderAddress = walletClient.account!.address;
const OWNER_WRITE_CONDITION = "0x4C9bFC96d7092b590D497A191826C3dA2277c34B";
const LICENSE_READ_CONDITION = "0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3";
const LICENSE_TOKEN = "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC";
const writeConditionData = encodeAbiParameters(
[{ type: "address" }],
[uploaderAddress],
);
const readConditionData = encodeAbiParameters(
[{ type: "address" }, { type: "address" }],
[LICENSE_TOKEN, ipId],
);
// Pure read
const globalPubKey = await client.observer.getGlobalPubKey();
const helia = await createHelia();
const storage = new HeliaProvider({
helia,
unixfs: unixfs(helia),
CID: (s) => CID.parse(s),
});
const sourceFile = await readFile("./example.pdf");
// Off-chain upload + 2 on-chain transactions
const { uuid, cid } = await client.uploader.uploadFile({
content: new Uint8Array(sourceFile),
storageProvider: storage,
globalPubKey,
updatable: false,
writeConditionAddr: OWNER_WRITE_CONDITION,
readConditionAddr: LICENSE_READ_CONDITION,
writeConditionData,
readConditionData,
accessAuxData: "0x",
});
// 1 on-chain read transaction + off-chain download + local AES decryption
const { content, txHash } = await client.consumer.downloadFile({
uuid,
accessAuxData: "0x",
storageProvider: storage,
timeoutMs: 120_000,
});
console.log(`Stored at CID: ${cid}`);
console.log(`Read tx: ${txHash}`);
await writeFile("./example.decrypted.pdf", Buffer.from(content));
console.log("Decrypted file written to ./example.decrypted.pdf");
```
`HeliaProvider` is the only storage backend fully tested on Aeneid in the
current release, and it requires Node.js 22+.
`uploadFile()` and `downloadFile()` work with raw file bytes. In a browser,
start from a `File` object and convert it with `new Uint8Array(await
file.arrayBuffer())`.
### Storage Providers
The encrypted-file workflow supports four storage backends:
* `HeliaProvider` for in-process IPFS. This is the best starting point for
development and the only backend fully tested on Aeneid so far.
* `GatewayProvider` for an external IPFS HTTP API plus a gateway URL.
* `StorachaProvider` for Storacha / `web3.storage`.
* `SynapseProvider` for Filecoin-backed storage via Synapse.
If you use `HeliaProvider`, pass the `CID.parse` function into the constructor
as shown above to avoid class mismatches.
## Step-by-Step (Low-Level)
If you need more control over the process, you can call each step individually.
These snippets continue from the variables in the examples above:
`walletClient`, `globalPubKey`, `requesterPubKey`, `recipientPrivKey`, and
`dataKey`.
### Encrypt (Low-Level)
```typescript theme={null}
import { uuidToLabel } from "@piplabs/cdr-sdk";
import { toHex } from "viem";
const walletAddress = walletClient.account!.address;
// On-chain transaction: allocate a vault using the wallet address as the
// write AND read condition. Only this EOA can write or read.
const { txHash: allocateTx, uuid } = await uploader.allocate({
updatable: false,
writeConditionAddr: walletAddress,
readConditionAddr: walletAddress,
writeConditionData: "0x",
readConditionData: "0x",
skipConditionValidation: true,
});
// Local: derive the label from the UUID
const label = uuidToLabel(uuid);
// Local: TDH2 encrypt the secret
const ciphertext = await uploader.encryptDataKey({
dataKey,
globalPubKey,
label,
});
// On-chain transaction: write encrypted data to the vault
const { txHash: writeTx } = await uploader.write({
uuid,
accessAuxData: "0x",
encryptedData: toHex(ciphertext.raw),
});
```
### Decrypt (Low-Level)
```typescript theme={null}
import { uuidToLabel } from "@piplabs/cdr-sdk";
// On-chain transaction: submit read request
const { txHash: readTx } = await consumer.read({
uuid,
accessAuxData: "0x",
requesterPubKey,
});
// Off-chain: poll the Story-API endpoint for validator partial decryptions.
// The required threshold is derived from the bucket's own DKG round.
const partials = await consumer.collectPartials({
uuid,
requesterPubKey, // the secp256k1 pubkey used in the read request
timeoutMs: 120_000,
});
// Pure read: fetch the vault ciphertext
const label = uuidToLabel(uuid);
const vault = await observer.getVault(uuid);
// Local: decrypt each partial, then combine them
const recoveredDataKey = await consumer.decryptDataKey({
ciphertext: {
raw: Uint8Array.from(Buffer.from(vault.encryptedData.slice(2), "hex")),
label,
},
partials,
recipientPrivKey,
globalPubKey,
label,
});
```
## Query DKG State
You can query DKG state and fees without a wallet or WASM initialization:
```typescript theme={null}
import { createPublicClient, http } from "viem";
import { CDRClient } from "@piplabs/cdr-sdk";
const publicClient = createPublicClient({
transport: http("https://aeneid.storyrpc.io"),
});
const client = new CDRClient({
network: "testnet",
publicClient,
apiUrl: "http://172.192.41.96:1317",
});
const threshold = await client.observer.getOperationalThreshold();
console.log("Operational threshold:", threshold);
const [allocateFee, writeFee, readFee] = await Promise.all([
client.observer.getAllocateFee(),
client.observer.getWriteFee(),
client.observer.getReadFee(),
]);
console.log(
`Fees — allocate: ${allocateFee}, write: ${writeFee}, read: ${readFee}`,
);
// Query a specific vault
const vault = await client.observer.getVault(1);
console.log("Vault:", vault);
```
## Understanding Fees
Each CDR operation has an on-chain fee:
| Operation | Fee Query | Description |
| --------- | --------------------------- | -------------------------------- |
| Allocate | `observer.getAllocateFee()` | One-time cost to create a vault |
| Write | `observer.getWriteFee()` | Cost per write to a vault |
| Read | `observer.getReadFee()` | Cost per read/decryption request |
Fees are paid in native tokens (wei) and are sent as `msg.value` with each transaction.
# IP Asset Vaults
Source: https://docs.story.foundation/developers/cdr-sdk/ip-asset-vaults
Learn how to create CDR vaults backed by IP Assets that require license tokens to decrypt.
CDR vaults can be gated behind Story Protocol license tokens so that only
license holders can decrypt the vault contents. In the current Aeneid release,
this is a manual integration: you encode the CDR condition data yourself and
mint Story license tokens separately.
### Prerequisites
* [CDR SDK setup](/developers/cdr-sdk/setup) complete
* `@story-protocol/core-sdk` installed if you plan to mint license tokens in code
* Familiarity with [IP Assets](/concepts/ip-asset) and [License Tokens](/concepts/licensing-module/license-token)
## Aeneid Contracts
| Contract | Address |
| -------------------- | -------------------------------------------- |
| OwnerWriteCondition | `0x4C9bFC96d7092b590D497A191826C3dA2277c34B` |
| LicenseReadCondition | `0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3` |
| LicenseToken | `0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC` |
## How the Story License Read Pattern Works
Every CDR vault has a `writeConditionAddr` and `readConditionAddr`. For a
Story license-gated vault on Aeneid:
* `writeConditionAddr` usually points at `OwnerWriteCondition`, with
`writeConditionData = abi.encode(ownerAddress)`
* `readConditionAddr` points at `LicenseReadCondition`, with
`readConditionData = abi.encode(licenseTokenAddress, ipId)`
* `accessAuxData` at read time is
`abi.encode(uint256[] licenseTokenIds)`
The CDR SDK does not register the IP Asset or mint the license token for you.
Create the IP and obtain its `ipId` with Story tooling first, then configure
the vault.
If you have not registered the asset yet, start with
[Register IP Asset](/developers/typescript-sdk/register-ip-asset). If you need
to attach or inspect license terms before minting, see
[Attach Terms](/developers/typescript-sdk/attach-terms).
## Upload a License-Gated Vault
```typescript theme={null}
import { encodeAbiParameters } from "viem";
const globalPubKey = await client.observer.getGlobalPubKey();
const dataKey = new TextEncoder().encode("confidential IP content");
const writeCondData = encodeAbiParameters(
[{ type: "address" }],
[uploaderAddress],
);
const readCondData = encodeAbiParameters(
[{ type: "address" }, { type: "address" }],
[
"0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC",
ipId,
],
);
await client.uploader.uploadCDR({
dataKey,
globalPubKey,
updatable: false,
writeConditionAddr: "0x4C9bFC96d7092b590D497A191826C3dA2277c34B",
writeConditionData: writeCondData,
readConditionAddr: "0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3",
readConditionData: readCondData,
accessAuxData: "0x",
});
```
The same condition setup works with `uploadFile()` if the encrypted content
lives off-chain.
## Mint a License Token Before Reading
Before a user can read a Story license-gated vault, they still need to mint a
license token. The Story core SDK's `wipClient` handles the WIP wrap and
approval steps so the reader can wrap IP, approve the RoyaltyModule, and mint
in three short calls.
**WIP** is **Wrapped IP**, the ERC-20 wrapped form of the native `IP` token.
Story's royalty / license flows use WIP, so the reader first wraps IP, then
approves the RoyaltyModule to spend it.
```typescript theme={null}
import { parseEther, http } from "viem";
import { StoryClient } from "@story-protocol/core-sdk";
const ROYALTY_MODULE = "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086";
const storyClient = StoryClient.newClient({
transport: http("https://aeneid.storyrpc.io"),
account: readerAccount,
chainId: "aeneid",
});
// 1. Wrap 1 IP → 1 WIP so the mint fee can be paid in WIP.
await storyClient.wipClient.deposit({
amount: parseEther("1"),
});
// 2. Approve the RoyaltyModule to spend WIP for the mint.
await storyClient.wipClient.approve({
spender: ROYALTY_MODULE,
amount: parseEther("1"),
});
// 3. Mint the Story license token.
const mintResult = await storyClient.license.mintLicenseTokens({
licensorIpId: ipId,
licenseTermsId: BigInt(2054),
amount: 1,
});
const licenseTokenId = mintResult.licenseTokenIds![0];
```
`licenseTermsId: 2054` is only an example. Replace it with the license terms
ID actually attached to your IP Asset. You receive that ID when you register
the asset or attach terms.
The wrap, approve, and mint calls above are all on-chain transactions. The
later `accessCDR()` call adds one more on-chain read request.
## Read with a License Token
At read time, pass the caller's license token ID through `accessAuxData`.
```typescript theme={null}
import { encodeAbiParameters } from "viem";
const accessAuxData = encodeAbiParameters(
[{ type: "uint256[]" }],
[[BigInt(licenseTokenId)]],
);
// Sends 1 read transaction, then collects partials and combines locally
const { dataKey } = await client.consumer.accessCDR({
uuid,
accessAuxData,
timeoutMs: 120_000,
});
const content = new TextDecoder().decode(dataKey);
console.log(`Decrypted IP content: ${content}`);
```
If the caller does not hold a valid license token for the vault's IP Asset,
the read request reverts on-chain and validators will not produce partial
decryptions.
## Custom Condition Contracts
License gating is just one pattern. You can deploy your own condition contract
for any access control logic by implementing one or both of these interfaces:
```solidity theme={null}
interface ICDRWriteCondition {
function checkWriteCondition(
uint32 uuid,
bytes calldata accessAuxData,
bytes calldata conditionData,
address caller
) external view returns (bool);
}
interface ICDRReadCondition {
function checkReadCondition(
uint32 uuid,
bytes calldata accessAuxData,
bytes calldata conditionData,
address caller
) external view returns (bool);
}
```
The CDR contract calls these functions before allowing a `write()` or `read()` operation. Return `true` to allow, `false` to deny. Then pass your contract's address as `readConditionAddr` or `writeConditionAddr` when allocating a vault.
# CDR SDK Overview
Source: https://docs.story.foundation/developers/cdr-sdk/overview
Learn how to integrate Confidential Data Rails (CDR) into your application using the CDR SDK.
These docs track the Aeneid release of `@piplabs/cdr-sdk` (`v0.2.1`),
available on npm.
Drop-in skill for Claude and other agents, plus three end-to-end examples
covering the on-chain secret, encrypted file, and IP-gated flows. The fastest
way to get a working CDR integration.
## What is CDR?
**Confidential Data Rails (CDR)** is Story's application layer for threshold-encrypted data on Story L1. Under the hood, it uses the validator network's DKG-generated public key so you can encrypt secrets such that no single party ever holds the complete decryption key. Data can only be decrypted when a threshold number of validators collectively provide partial decryptions, with access control enforced on-chain via smart contracts. The validator-side DKG and partial decryption flows run inside `story-kernel` TEEs (Intel SGX enclaves).
CDR enables powerful use cases like:
* **Secret sharing** - encrypt and share secrets that only specific wallets can decrypt
* **Encrypted file delivery** - keep large files off-chain while storing the encrypted file key on-chain
* **Data marketplaces** - sell access to encrypted data with on-chain payment enforcement
* **IP-gated content** - tie encrypted data to IP Assets and require license tokens to decrypt
## Security and Trust Model
* **Confidentiality** - Vault payloads stay encrypted unless a threshold number
of validators participate in decryption and the read condition passes.
* **Metadata visibility** - Vault UUIDs, condition addresses, transactions, and
any off-chain storage pointers you disclose are not hidden by CDR.
* **Availability** - Reads can fail if enough validators do not respond before
timeout. In that case, retry the read request or increase `timeoutMs`.
* **Forward secrecy / revocation** - Treat CDR ciphertext as bound to the
access rules and validator set in effect when you encrypted it. If your
access model changes, rotate or re-encrypt the content at the application
layer.
* **Release posture** - The current public release runs on Aeneid testnet.
Build and test integrations there, but do not treat it as a production
confidentiality environment.
## What Ships in the Aeneid Release
The current SDK surface is centered around two workflows:
* **Data key vaults** via `uploadCDR` / `accessCDR` for small secrets stored directly on-chain
* **Encrypted files** via `uploadFile` / `downloadFile` for off-chain content with on-chain key management
The Aeneid release also includes:
* `observer`, `uploader`, and `consumer` sub-clients
* DKG state reads over the Story-API REST endpoint (`apiUrl`)
* Storage providers for Helia, gateway-backed IPFS, Storacha, and Synapse
* Validator registration, attestation queries, and SGX attestation verification utilities
## How It Works
CDR revolves around **vaults**. Each vault stores encrypted data and has two configurable access control conditions:
* **Write Condition** - determines who can store encrypted data in the vault
* **Read Condition** - determines who can request decryption of the vault's data
When `msg.sender` equals the configured condition address, the CDR contract
bypasses the condition check — so setting your own wallet address as the
condition makes a vault owner-only (other callers revert, since an EOA does
not implement `checkWriteCondition` / `checkReadCondition`). The SDK validates
condition addresses by default; when you intentionally use an EOA this way,
call `allocate()` with `skipConditionValidation: true`.
There are two common ways to use a vault:
* **On-chain secret**: store the encrypted bytes directly in the vault
* **Off-chain file**: store an encrypted file in a storage backend and keep the
encrypted AES key plus content pointer in the vault
### Data Key Vault Flow
1. **Allocate** a vault on-chain with your desired read/write conditions
2. **Fetch** the DKG global public key from the validator network
3. **Encrypt** your data locally using TDH2 threshold encryption
4. **Write** the encrypted ciphertext to the vault on-chain
Walkthrough:
[Encrypt a Secret](/developers/cdr-sdk/encrypt-and-decrypt#encrypt-a-secret).
### Encrypted File Flow
**Upload (data owner):**
1. **Encrypt** the file locally with an AES key
2. **Upload** the encrypted file to a storage backend such as IPFS
3. **Encrypt** the AES key plus CID through CDR and write the resulting vault
payload
**Download (authorized reader):**
1. **Read** the vault and recover the AES key payload through threshold
decryption
2. **Download** the encrypted file from storage
3. **Decrypt** the file client-side with the recovered AES key
Walkthrough:
[Encrypt and Download a File](/developers/cdr-sdk/encrypt-and-decrypt#encrypt-and-download-a-file).
### Decryption Flow
1. **Generate** an ephemeral keypair for the decryption session
2. **Submit** a read request on-chain (validated against the read condition)
3. **Collect** partial decryptions from validators until you meet threshold
4. **Combine** the partials client-side to recover the original data key
Plaintext encryption and final decryption happen **client-side**. Validators only produce TEE-confined partial decryptions, and neither the CDR contract nor validators ever see your plaintext data.
## Access Control Patterns
### Wallet Address (Simple)
Set your wallet address as the read/write condition. Only you can encrypt/decrypt.
```typescript theme={null}
await uploader.allocate({
updatable: false,
writeConditionAddr: userAddress, // only you can write
readConditionAddr: userAddress, // only you can read
writeConditionData: "0x",
readConditionData: "0x",
skipConditionValidation: true,
});
```
For an end-to-end example, see
[Encrypt a Secret](/developers/cdr-sdk/encrypt-and-decrypt#encrypt-a-secret).
This EOA shortcut is most useful with `allocate()`. The high-level
`uploadCDR()` / `uploadFile()` helpers validate condition contracts and
therefore use the deployed owner-only condition contract in the examples.
### License Token (IP-Gated)
Use the deployed `LicenseReadCondition` contract on Aeneid and encode
`abi.encode(licenseTokenAddress, ipId)` as `readConditionData`. The vault
writer typically uses the deployed `OwnerWriteCondition` contract so only the
uploader can write, while readers must present valid Story license token IDs in
`accessAuxData`.
Technical walkthrough:
[How the Story License Read Pattern Works](/developers/cdr-sdk/ip-asset-vaults#how-the-story-license-read-pattern-works).
### Custom Condition Contracts
Deploy your own condition contract implementing `checkReadCondition` and `checkWriteCondition` for advanced access control like:
* **Fixed fee** - pay a one-time fee to unlock read access
* **Time-based** - access only during a specific time window
* **Marketplace** - listing owner controls writes, purchasers can read
### Condition Helpers
The SDK includes helper encoders for common access patterns:
```typescript theme={null}
import { conditions } from "@piplabs/cdr-sdk";
conditions.ownerOnly({ address: conditionAddr, owner: "0x..." });
conditions.custom({ address: conditionAddr, conditionData: "0x..." });
conditions.open({ address: conditionAddr });
conditions.tokenGate({ address: conditionAddr, token: "0x...", minBalance: 1n });
conditions.merkle({ address: conditionAddr, root: "0x..." });
```
On Aeneid, `ownerOnly()` and `custom()` are the practical built-in patterns
today. `open()`, `tokenGate()`, and `merkle()` help encode condition data,
but you still need to deploy a matching condition contract yourself.
`conditions.storyLicense()` is not available yet.
For the deployed Story license-gated pattern, see
[How the Story License Read Pattern Works](/developers/cdr-sdk/ip-asset-vaults#how-the-story-license-read-pattern-works).
## Next Steps
Install the SDK from npm and initialize the client for Aeneid.
DKG backends, validation RPCs, system addresses, and release notes.
Use both the on-chain secret and encrypted-file workflows.
Configure license-gated reads with Story license tokens on Aeneid.
Full API reference for every CDR SDK method.
Install the CDR skill for your AI agent and explore three end-to-end examples.
# Setup CDR Client
Source: https://docs.story.foundation/developers/cdr-sdk/setup
Learn how to install and configure the CDR SDK.
These docs track the Aeneid release of `@piplabs/cdr-sdk` (`v0.2.1`).
### Prerequisites
* Node.js 18+ and npm 8+
* Node.js 22+ if you plan to use `HeliaProvider`
* A funded wallet on Aeneid testnet
* [viem](https://www.npmjs.com/package/viem) (v2.21+) for blockchain interactions
## Install
```bash npm theme={null}
npm install @piplabs/cdr-sdk viem
```
```bash pnpm theme={null}
pnpm add @piplabs/cdr-sdk viem
```
```bash yarn theme={null}
yarn add @piplabs/cdr-sdk viem
```
`viem` (v2.21+) is a required peer dependency.
Storage providers are optional and pull their own peer dependencies. Install
them only for the backend you use: `helia`, `multiformats`, and
`@helia/unixfs` for `HeliaProvider`, `@storacha/client` for `StorachaProvider`,
or `@filoz/synapse-sdk` for `SynapseProvider`.
If you plan to mint Story license tokens in an IP-gated flow, also install
`@story-protocol/core-sdk`.
## Initialize WASM
The CDR SDK uses a WebAssembly module for threshold cryptography. You must initialize it once before performing any encryption or decryption operations.
```typescript theme={null}
import { initWasm } from "@piplabs/cdr-sdk";
// Call once at application startup
await initWasm();
```
In a React application, initialize WASM in a provider component or top-level
effect so it's ready before any CDR operations are attempted.
## Browser and Bundler Guidance
* **Vite / webpack** - Import the SDK from normal ESM application code and call
`initWasm()` before the first encryption or decryption. If your SSR build
tries to evaluate the SDK server-side, move the import behind a client-only
boundary.
* **Next.js / SSR** - Keep browser wallet flows in `"use client"` components.
For route handlers or scripts that use CDR cryptography, run them in the Node
runtime instead of Edge.
* **Edge runtime** - The current release is not documented for Edge runtimes.
Prefer the browser or Node.js runtime on Aeneid.
* **TypeScript** - Use modern ESM resolution. `moduleResolution: "Bundler"` is
a good default for browser apps; `moduleResolution: "NodeNext"` fits pure
Node ESM projects.
```typescript theme={null}
// Next.js route handlers / server actions
export const runtime = "nodejs";
```
## Story-API REST Endpoint
Every `CDRClient` requires an `apiUrl` — the base URL of a Story-API REST
endpoint. The SDK reads all DKG state (active round, global public key,
threshold, participant count, registered validators, and validator
attestations) over this REST API. Contract state such as vaults and fees is
still read over the EVM `publicClient`.
| Network | Story-API REST URL | Notes |
| ------- | --------------------------- | ------------------------------------------- |
| Aeneid | `http://172.192.41.96:1317` | Plain HTTP. May change between deployments. |
For production deployments you can point `apiUrl` at your own Story node's
REST gateway instead of the shared endpoint. Configure it through an
environment variable so it is easy to swap.
## Create the CDR Client
The `CDRClient` provides three sub-clients:
* **`observer`** - Read-only queries (fees, vault data, DKG state). Always available.
* **`uploader`** - Encryption and vault allocation. Requires a `walletClient`.
* **`consumer`** - Decryption and read requests. Requires a `walletClient`.
### In React (Wallet Connector)
In a React app, you typically get the wallet from a connector like Privy, RainbowKit, or wagmi. Create a read-only `CDRClient` up front, and build a write-capable client on demand from the wallet's provider.
```typescript hooks/use-cdr-client.ts theme={null}
import { useMemo } from "react";
import { createPublicClient, createWalletClient, custom, http } from "viem";
import { CDRClient } from "@piplabs/cdr-sdk";
// Example using Privy — adapt for your wallet connector
import { usePrivy, useWallets } from "@privy-io/react-auth";
export function useCDRClient() {
const { authenticated } = usePrivy();
const { wallets } = useWallets();
const wallet = wallets[0];
// Read-only client — always available
const publicClient = useMemo(
() =>
createPublicClient({ transport: http(process.env.NEXT_PUBLIC_RPC_URL) }),
[],
);
const apiUrl = process.env.NEXT_PUBLIC_STORY_API_URL!;
const client = useMemo(
() => new CDRClient({ network: "testnet", publicClient, apiUrl }),
[publicClient, apiUrl],
);
// Write client — created on demand from the wallet's provider
const getWriteClient = async () => {
if (!wallet) throw new Error("No wallet connected");
const provider = await wallet.getEthereumProvider();
const walletClient = createWalletClient({
transport: custom(provider),
account: wallet.address as `0x${string}`,
});
return new CDRClient({
network: "testnet",
publicClient,
walletClient,
apiUrl,
});
};
return { client, publicClient, getWriteClient, address: wallet?.address };
}
```
Then in your components:
```typescript theme={null}
const { client, getWriteClient } = useCDRClient();
// Read-only operations work immediately
const vault = await client.observer.getVault(42);
// Write operations — get a write client first
const writeClient = await getWriteClient();
await writeClient.uploader.write({ uuid, accessAuxData: "0x", encryptedData });
```
### With Private Key (Backend / Scripts)
For server-side code, scripts, or CLI tools, you can use a private key directly:
```typescript theme={null}
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { CDRClient } from "@piplabs/cdr-sdk";
const account = privateKeyToAccount(`0x${process.env.WALLET_PRIVATE_KEY}`);
const publicClient = createPublicClient({
transport: http(process.env.RPC_PROVIDER_URL),
});
const walletClient = createWalletClient({
account,
transport: http(process.env.RPC_PROVIDER_URL),
});
const client = new CDRClient({
network: "testnet",
publicClient,
walletClient,
apiUrl: process.env.STORY_API_URL!,
});
```
### Read-Only (No Wallet)
If you only need to query vault data or DKG state, you can omit the `walletClient`:
```typescript theme={null}
const client = new CDRClient({
network: "testnet",
publicClient,
apiUrl: process.env.STORY_API_URL!,
});
// observer methods work without a wallet
const vault = await client.observer.getVault(123);
const allocateFee = await client.observer.getAllocateFee();
```
Attempting to use `client.uploader` or `client.consumer` without a
`walletClient` will throw a `WalletClientRequiredError`.
## Network Configuration
### Supported Network
| Network | `network` param | Default RPC URL | Story-API REST URL | Description |
| ------- | --------------- | ---------------------------- | --------------------------- | ------------------------- |
| Aeneid | `"testnet"` | `https://aeneid.storyrpc.io` | `http://172.192.41.96:1317` | Current supported release |
```typescript Testnet theme={null}
const publicClient = createPublicClient({
transport: http("https://aeneid.storyrpc.io"),
});
const client = new CDRClient({
network: "testnet",
publicClient,
apiUrl: "http://172.192.41.96:1317",
});
```
### Custom RPC URL
You can point the SDK to any Aeneid-compatible RPC endpoint by changing the
`http()` transport URL. This is useful for third-party RPC providers with
higher rate limits. The `apiUrl` is configured independently — point it at the
shared Story-API endpoint or your own Story node's REST gateway.
```typescript theme={null}
const publicClient = createPublicClient({
transport: http("https://your-aeneid-rpc.example.com"),
});
const walletClient = createWalletClient({
account,
transport: http("https://your-aeneid-rpc.example.com"),
});
// Use "testnet"
const client = new CDRClient({
network: "testnet",
publicClient,
walletClient,
apiUrl: "http://172.192.41.96:1317",
});
```
### Using Environment Variables
A common pattern is to configure the network via environment variables:
```typescript config.ts theme={null}
const RPC_URL = process.env.RPC_URL ?? "https://aeneid.storyrpc.io";
const STORY_API_URL = process.env.STORY_API_URL ?? "http://172.192.41.96:1317";
const NETWORK = (process.env.NETWORK ?? "testnet") as "testnet";
const publicClient = createPublicClient({ transport: http(RPC_URL) });
const client = new CDRClient({
network: NETWORK,
publicClient,
apiUrl: STORY_API_URL,
});
```
```bash .env theme={null}
# Testnet (default)
RPC_URL=https://aeneid.storyrpc.io
STORY_API_URL=http://172.192.41.96:1317
NETWORK=testnet
# Alternate Aeneid RPC
RPC_URL=https://your-aeneid-rpc.example.com
STORY_API_URL=http://your-story-node:1317
NETWORK=testnet
```
## Quick Start: End-to-End Secret Example
The script below creates an owner-only vault, writes a small secret, then reads
it back with the same wallet. It is fully runnable once `WALLET_PRIVATE_KEY` is
set.
```typescript quickstart-cdr.ts theme={null}
import { CDRClient, initWasm, uuidToLabel } from "@piplabs/cdr-sdk";
import {
createPublicClient,
createWalletClient,
http,
toHex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
const RPC_URL = process.env.RPC_URL ?? "https://aeneid.storyrpc.io";
const STORY_API_URL = process.env.STORY_API_URL ?? "http://172.192.41.96:1317";
const PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY as `0x${string}` | undefined;
if (!PRIVATE_KEY) {
throw new Error("Set WALLET_PRIVATE_KEY before running this script.");
}
const account = privateKeyToAccount(PRIVATE_KEY);
const publicClient = createPublicClient({ transport: http(RPC_URL) });
const walletClient = createWalletClient({
account,
transport: http(RPC_URL),
});
await initWasm();
const client = new CDRClient({
network: "testnet",
publicClient,
walletClient,
apiUrl: STORY_API_URL,
});
// Use the wallet (EOA) address as both write and read condition. Only this
// EOA can encrypt to or decrypt from the vault.
const { uuid, txHash: allocateTx } = await client.uploader.allocate({
updatable: false,
writeConditionAddr: account.address,
readConditionAddr: account.address,
writeConditionData: "0x",
readConditionData: "0x",
skipConditionValidation: true,
});
const globalPubKey = await client.observer.getGlobalPubKey();
const ciphertext = await client.uploader.encryptDataKey({
dataKey: new TextEncoder().encode("hello from CDR"),
globalPubKey,
label: uuidToLabel(uuid),
});
const { txHash: writeTx } = await client.uploader.write({
uuid,
accessAuxData: "0x",
encryptedData: toHex(ciphertext.raw),
});
console.log("Vault UUID:", uuid);
console.log("Allocate tx:", allocateTx);
console.log("Write tx:", writeTx);
const { dataKey, txHash } = await client.consumer.accessCDR({
uuid,
accessAuxData: "0x",
timeoutMs: 120_000,
});
console.log("Read tx:", txHash);
console.log("Recovered secret:", new TextDecoder().decode(dataKey));
```
This example sends three transactions total: `allocate()`, `write()`, and
`read()`. For larger payloads, switch to `uploadFile()` / `downloadFile()`
with deployed condition contracts (such as the Story license-gated pattern
in [IP Asset Vaults](/developers/cdr-sdk/ip-asset-vaults)).
Any EOA address works as a write or read condition — only that EOA can
perform the matching action. The high-level `uploadCDR()` / `uploadFile()`
helpers validate that condition addresses point at deployed contracts, so
EOA conditions go through the low-level `allocate()` call with
`skipConditionValidation: true`.
## Next Steps
* For network-side runtime behavior, see
[Runtime Configuration](/developers/cdr-sdk/advanced-configuration).
* For the main integration flows, continue to
[Encrypt & Decrypt](/developers/cdr-sdk/encrypt-and-decrypt).
## Error Handling
The SDK throws typed errors you can catch and handle:
| Error Class | Code | When |
| ------------------------------- | ---------------------------- | -------------------------------------------------------------------------- |
| `CDRError` | varies | Base class for all SDK-specific errors |
| `WalletClientRequiredError` | `WALLET_CLIENT_REQUIRED` | Accessing `uploader` or `consumer` without a `walletClient` |
| `InvalidParamsError` | `INVALID_PARAMS` | Invalid parameter combinations, such as only passing one keypair parameter |
| `InvalidConditionContractError` | `INVALID_CONDITION_CONTRACT` | Condition address does not implement the required interface |
| `LabelMismatchError` | `LABEL_MISMATCH` | Ciphertext label does not match the vault UUID |
| `ContentSizeExceededError` | `CONTENT_SIZE_EXCEEDED` | Encrypted data exceeds `maxEncryptedDataSize` |
| `EmptyVaultError` | `EMPTY_VAULT` | Reading a vault that has never been written to |
| `PartialCollectionTimeoutError` | `PARTIAL_COLLECTION_TIMEOUT` | `collectPartials` or `accessCDR` times out waiting for validator responses |
| `CidIntegrityError` | `CID_INTEGRITY` | Downloaded encrypted file does not match the vault CID |
On-chain transaction reverts (for example, a failed condition check) surface
as the underlying `viem` contract errors, not a CDR-specific error class.
All errors extend `CDRError`, which has a `code` property for programmatic handling:
```typescript theme={null}
import { CDRError, PartialCollectionTimeoutError } from "@piplabs/cdr-sdk";
try {
const { dataKey } = await client.consumer.accessCDR({ ... });
} catch (err) {
if (err instanceof PartialCollectionTimeoutError) {
console.error("Not enough validators responded in time. Try increasing timeoutMs.");
} else if (err instanceof CDRError) {
console.error(`CDR error [${err.code}]: ${err.message}`);
}
}
```
# Deployed Smart Contracts
Source: https://docs.story.foundation/developers/deployed-smart-contracts
A list of all deployed protocol addresses
## Core Protocol Contracts
* View contracts on our GitHub [here](https://github.com/storyprotocol/protocol-core-v1/tree/main)
```json Aeneid Testnet theme={null}
{
"AccessController": "0xcCF37d0a503Ee1D4C11208672e622ed3DFB2275a",
"ArbitrationPolicyUMA": "0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936",
"CoreMetadataModule": "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16",
"CoreMetadataViewModule": "0xB3F88038A983CeA5753E11D144228Ebb5eACdE20",
"DisputeModule": "0x9b7A9c70AFF961C799110954fc06F3093aeb94C5",
"EvenSplitGroupPool": "0xf96f2c30b41Cb6e0290de43C8528ae83d4f33F89",
"GroupNFT": "0x4709798FeA84C84ae2475fF0c25344115eE1529f",
"GroupingModule": "0x69D3a7aa9edb72Bc226E745A7cCdd50D947b69Ac",
"IPAccountImplBeacon": "0x9825cc7A398D9C3dDD66232A8Ec76d5b05422581",
"IPAccountImplBeaconProxy": "0x00b800138e4D82D1eea48b414d2a2A8Aee9A33b1",
"IPAccountImpl": "0xdeC03e0c63f800efD7C9d04A16e01E80cF57Bf79",
"IPAssetRegistry": "0x77319B4031e6eF1250907aa00018B8B1c67a244b",
"IPGraphACL": "0x1640A22a8A086747cD377b73954545e2Dfcc9Cad",
"IpRoyaltyVaultBeacon": "0x6928ba25Aa5c410dd855dFE7e95713d83e402AA6",
"IpRoyaltyVaultImpl": "0xbd0f3c59B6f0035f55C58893fA0b1Ac4aDEa50Dc",
"LicenseRegistry": "0x529a750E02d8E2f15649c13D69a465286a780e24",
"LicenseToken": "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC",
"LicensingModule": "0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f",
"ModuleRegistry": "0x022DBAAeA5D8fB31a0Ad793335e39Ced5D631fa5",
"PILicenseTemplate": "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316",
"ProtocolAccessManager": "0xFdece7b8a2f55ceC33b53fd28936B4B1e3153d53",
"ProtocolPauseAdmin": "0xdd661f55128A80437A0c0BDA6E13F214A3B2EB24",
"RoyaltyModule": "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086",
"RoyaltyPolicyLAP": "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E",
"RoyaltyPolicyLRP": "0x9156e603C949481883B1d3355c6f1132D191fC41"
}
```
```json Mainnet theme={null}
{
"AccessController": "0xcCF37d0a503Ee1D4C11208672e622ed3DFB2275a",
"ArbitrationPolicyUMA": "0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936",
"CoreMetadataModule": "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16",
"CoreMetadataViewModule": "0xB3F88038A983CeA5753E11D144228Ebb5eACdE20",
"DisputeModule": "0x9b7A9c70AFF961C799110954fc06F3093aeb94C5",
"EvenSplitGroupPool": "0xf96f2c30b41Cb6e0290de43C8528ae83d4f33F89",
"GroupNFT": "0x4709798FeA84C84ae2475fF0c25344115eE1529f",
"GroupingModule": "0x69D3a7aa9edb72Bc226E745A7cCdd50D947b69Ac",
"IPAccountImplBeacon": "0x9825cc7A398D9C3dDD66232A8Ec76d5b05422581",
"IPAccountImplBeaconProxy": "0x00b800138e4D82D1eea48b414d2a2A8Aee9A33b1",
"IPAccountImpl": "0x7343646585443F1c3F64E4F08b708788527e1C77",
"IPAssetRegistry": "0x77319B4031e6eF1250907aa00018B8B1c67a244b",
"IPGraphACL": "0x1640A22a8A086747cD377b73954545e2Dfcc9Cad",
"IpRoyaltyVaultBeacon": "0x6928ba25Aa5c410dd855dFE7e95713d83e402AA6",
"IpRoyaltyVaultImpl": "0x63cC7611316880213f3A4Ba9bD72b0EaA2010298",
"LicenseRegistry": "0x529a750E02d8E2f15649c13D69a465286a780e24",
"LicenseToken": "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC",
"LicensingModule": "0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f",
"ModuleRegistry": "0x022DBAAeA5D8fB31a0Ad793335e39Ced5D631fa5",
"PILicenseTemplate": "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316",
"ProtocolAccessManager": "0xFdece7b8a2f55ceC33b53fd28936B4B1e3153d53",
"ProtocolPauseAdmin": "0xdd661f55128A80437A0c0BDA6E13F214A3B2EB24",
"RoyaltyModule": "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086",
"RoyaltyPolicyLAP": "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E",
"RoyaltyPolicyLRP": "0x9156e603C949481883B1d3355c6f1132D191fC41"
}
```
## Periphery Contracts
* View contracts on our GitHub [here](https://github.com/storyprotocol/protocol-periphery-v1)
```json Aeneid Testnet theme={null}
{
"DerivativeWorkflows": "0x9e2d496f72C547C2C535B167e06ED8729B374a4f",
"GroupingWorkflows": "0xD7c0beb3aa4DCD4723465f1ecAd045676c24CDCd",
"LicenseAttachmentWorkflows": "0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8",
"OwnableERC20Beacon": "0xB83639aF55F03108091020b7c75a46e2eaAb4FfA",
"OwnableERC20Template": "0xf8D299af9CBEd49f50D7844DDD1371157251d0A7",
"RegistrationWorkflows": "0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424",
"RoyaltyTokenDistributionWorkflows": "0xa38f42B8d33809917f23997B8423054aAB97322C",
"RoyaltyWorkflows": "0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890",
"SPGNFTBeacon": "0xD2926B9ecaE85fF59B6FB0ff02f568a680c01218",
"SPGNFTImpl": "0x5266215a00c31AaA2f2BB7b951Ea0028Ea8b4e37",
"TokenizerModule": "0xAC937CeEf893986A026f701580144D9289adAC4C"
}
```
```json Mainnet theme={null}
{
"DerivativeWorkflows": "0x9e2d496f72C547C2C535B167e06ED8729B374a4f",
"GroupingWorkflows": "0xD7c0beb3aa4DCD4723465f1ecAd045676c24CDCd",
"LicenseAttachmentWorkflows": "0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8",
"OwnableERC20Beacon": "0x9a81C447C0b4C47d41d94177AEea3511965d3Bc9",
"OwnableERC20Template": "0xE6505ffc5A7C19B68cEc2311Cc35BC02d8f7e0B1",
"RegistrationWorkflows": "0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424",
"RoyaltyTokenDistributionWorkflows": "0xa38f42B8d33809917f23997B8423054aAB97322C",
"RoyaltyWorkflows": "0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890",
"SPGNFTBeacon": "0xD2926B9ecaE85fF59B6FB0ff02f568a680c01218",
"SPGNFTImpl": "0x6Cfa03Bc64B1a76206d0Ea10baDed31D520449F5",
"TokenizerModule": "0xAC937CeEf893986A026f701580144D9289adAC4C"
}
```
## License Hooks
* View contracts on our GitHub [here](https://github.com/storyprotocol/protocol-periphery-v1/tree/main/contracts/hooks)
```json Aeneid Testnet theme={null}
{
"LockLicenseHook": "0x54C52990dA304643E7412a3e13d8E8923cD5bfF2",
"TotalLicenseTokenLimitHook": "0xaBAD364Bfa41230272b08f171E0Ca939bD600478"
}
```
```json Mainnet theme={null}
{
"LockLicenseHook": "0x5D874d4813c4A8A9FB2AB55F30cED9720AEC0222",
"TotalLicenseTokenLimitHook": "0xB72C9812114a0Fc74D49e01385bd266A75960Cda"
}
```
## Whitelisted Revenue Tokens
The below list contains the whitelisted revenue tokens that can be used in the Royalty Module. Learn more about Revenue Tokens [here](/concepts/royalty-module/ip-royalty-vault).
| Token | Contract Address | Explorer | Mint |
| :----- | :------------------------------------------- | :--------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| WIP | `0x1514000000000000000000000000000000000000` | [View here ↗️](https://aeneid.storyscan.io/address/0x1514000000000000000000000000000000000000) | N/A |
| MERC20 | `0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E` | [View here ↗️](https://aeneid.storyscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E) | [Mint ↗️](https://aeneid.storyscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E?tab=write_contract#0x40c10f19) |
| Token | Contract Address | Explorer | Mint |
| :---- | :------------------------------------------- | :------------------------------------------------------------------------------------------ | :--- |
| WIP | `0x1514000000000000000000000000000000000000` | [View here ↗️](https://www.storyscan.io/address/0x1514000000000000000000000000000000000000) | N/A |
## Misc
* **Multicall3**: 0xcA11bde05977b3631167028862bE2a173976CA11
* **Default License Terms ID** (Non-Commercial Social Remixing): 1
* **Bridged USDC (Stargate)**: 0xF1815bd50389c46847f0Bda824eC8da914045D14
We only support the above USDC on Story's mainnet.
## Ecosystem Official Contracts
The below is a list of official ecosystem contracts.
### Story ENS
```json Aeneid Testnet theme={null}
{
"SidRegistry": "0x5dC881dDA4e4a8d312be3544AD13118D1a04Cb17",
"PublicResolver": "0x6D3B3F99177FB2A5de7F9E928a9BD807bF7b5BAD"
}
```
```json Mainnet theme={null}
{
"SidRegistry": "0x5dC881dDA4e4a8d312be3544AD13118D1a04Cb17",
"PublicResolver": "0x6D3B3F99177FB2A5de7F9E928a9BD807bF7b5BAD"
}
```
# Dev Overview
Source: https://docs.story.foundation/developers/overview
For developers who want to build on our protocol.
If you're a developer, here is everything you need:
Can't find something? Ask the writer of our docs for help in our [Builder Discord](https://discord.gg/storybuilders).
View all testnet block & transaction data on Story.
View testnet transaction data specifically related to IP interactions like
registering, licensing, etc.
Start building on Story quickly.
## SDK
Check out the following resources to learn the SDK:
* [SDK Reference](/sdk-reference) - view the entire SDK reference with detailed **explanations and examples** for each function ***(includes working code so you can jump right to coding)***
* [TypeScript SDK Guide](/developers/typescript-sdk/overview) - a detailed, step-by-step walkthrough of how to set up and implement the most popular uses of the SDK (register IP, attach terms, register derivative, etc) ***(includes working code so you can jump right to coding)***
* [Tutorials](/developers/tutorials/overview) - more specific, external topics/tutorials that may be popular questions, like "How do I register music on Story?", "How do I implement wallet-less onboarding with the SDK?", etc ***(includes working code so you can jump right to coding)***
## Smart Contracts
Check out the following resources to learn the protocol:
* [Smart Contract Guide](/developers/smart-contracts-guide/overview) - a walkthrough of how to set up and implement the most popular uses of the protocol ***(includes working code so you can jump right to coding)***
* [Deployed Smart Contracts](/developers/deployed-smart-contracts) - all the deployed protocol addresses
* [Tutorials](/developers/tutorials/overview) - includes tutorials that cover more specific topics, like registering music, registering AI-generated images, etc ***(includes working code so you can jump right to coding)***
Do not use `RANDAO` for pseudo-randomness, instead use onchain VRF (Pyth or
Gelato). Currently, `RANDAO` value is set as the parent block hash and thus is
not random for X-1 block.
## API
View our [API Reference](/api-reference).
# React Guide
Source: https://docs.story.foundation/developers/react-guide/overview
Learn how to integrate the TypeScript SDK to work with React-based apps.
The best way to get started is to get your hands dirty and start building.
A working code example that shows setting up & calling TypeScript SDK
functions in Next.js/React.
View the whole SDK reference, which shows examples and types for every function in our SDK.
In the following series of tutorials, you will learn how to setup the TypeScript SDK in React.
# Dynamic Setup
Source: https://docs.story.foundation/developers/react-guide/setup/dynamic-setup
Learn how to setup Dynamic Wallet in your Story DApp.
**Optional: Official Dynamic Docs**
Check out the official Wagmi + Dynamic installation docs [here](https://docs.dynamic.xyz/react-sdk/using-wagmi).
## Install the Dependencies
```bash npm theme={null}
npm install --save @story-protocol/core-sdk viem wagmi @dynamic-labs/sdk-react-core @dynamic-labs/wagmi-connector @dynamic-labs/ethereum @tanstack/react-query
```
```bash pnpm theme={null}
pnpm install @story-protocol/core-sdk viem
```
```bash yarn theme={null}
yarn add @story-protocol/core-sdk viem
```
## Setup
Before diving into the example, make sure you have two things setup:
1. Make sure to have `NEXT_PUBLIC_RPC_PROVIDER_URL` set up in your `.env` file.
* You can use the public default one (`https://aeneid.storyrpc.io`) or any other RPC [here](/network/network-info/aeneid#rpcs).
2. Make sure to have `NEXT_PUBLIC_DYNAMIC_ENV_ID` set up in your `.env` file. Do this by logging into [Dynamic](https://app.dynamic.xyz/) and creating a project.
```jsx Web3Providers.tsx theme={null}
"use client";
import { createConfig, WagmiProvider } from "wagmi";
import { http } from 'viem';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core";
import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector";
import { EthereumWalletConnectors } from "@dynamic-labs/ethereum";
import { PropsWithChildren } from "react";
import { aeneid } from "@story-protocol/core-sdk";
// setup wagmi
const config = createConfig({
chains: [aeneid],
multiInjectedProviderDiscovery: false,
transports: {
[aeneid.id]: http(),
},
});
const queryClient = new QueryClient();
export default function Web3Providers({ children }: PropsWithChildren) {
return (
// setup dynamic
{children}
);
}
```
```jsx layout.tsx theme={null}
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { PropsWithChildren } from "react";
import Web3Providers from "./Web3Providers";
import { DynamicWidget } from "@dynamic-labs/sdk-react-core";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Example",
description: "This is an Example DApp",
};
export default function RootLayout({ children }: PropsWithChildren) {
return (
{children}
);
}
```
```jsx TestComponent.tsx theme={null}
import { custom, toHex } from 'viem';
import { useWalletClient } from "wagmi";
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
// example of how you would now use the fully setup sdk
export default function TestComponent() {
const { data: wallet } = useWalletClient();
async function setupStoryClient(): Promise {
const config: StoryConfig = {
wallet: wallet,
transport: custom(wallet!.transport),
chainId: "aeneid",
};
const client = StoryClient.newClient(config);
return client;
}
async function registerIp() {
const client = await setupStoryClient();
const response = await client.ipAsset.registerIpAsset({
nft: {
type: 'minted',
nftContract: '0x01...',
tokenId: '1',
}
ipMetadata: {
ipMetadataURI: "test-metadata-uri",
ipMetadataHash: toHex("test-metadata-hash", { size: 32 }),
nftMetadataURI: "test-nft-metadata-uri",
nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }),
}
});
console.log(
`Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}`
);
}
return (
{/* */}
)
}
```
# React Setup
Source: https://docs.story.foundation/developers/react-guide/setup/overview
Learn how to setup the TypeScript SDK in React.
We do not have a specific React SDK, however **we can use the TypeScript SDK in React** all the same, to delay signing & sending transactions to a JSON-RPC account like Metamask.
We recommend using [wagmi](https://wagmi.sh/) as a Web3 provider and then installing a wallet provider like Dynamic or RainbowKit. We provide examples for all of the following:
* [Dynamic Setup](/developers/react-guide/setup/dynamic-setup)
* [RainbowKit Setup](/developers/react-guide/setup/rainbowkit-setup)
* [Reown (WalletConnect) Setup](/developers/react-guide/setup/reown-setup)
* [Tomo Setup](/developers/react-guide/setup/tomo-setup)
# RainbowKit Setup
Source: https://docs.story.foundation/developers/react-guide/setup/rainbowkit-setup
Learn how to setup RainbowKit Wallet in your Story DApp.
**Optional: Official RainbowKit Docs**
Check out the official Wagmi + RainbowKit installation docs [here](https://www.rainbowkit.com/docs/installation).
## Install the Dependencies
```bash npm theme={null}
npm install --save @story-protocol/core-sdk @rainbow-me/rainbowkit wagmi viem @tanstack/react-query
```
```bash pnpm theme={null}
pnpm install @story-protocol/core-sdk viem
```
```bash yarn theme={null}
yarn add @story-protocol/core-sdk viem
```
## Setup
Before diving into the example, make sure you have two things setup:
1. Make sure to have `NEXT_PUBLIC_RPC_PROVIDER_URL` set up in your `.env` file.
* You can use the public default one (`https://aeneid.storyrpc.io`) or any other RPC [here](/network/network-info/aeneid#rpcs).
2. Make sure to have `NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` set up in your `.env` file. Do this by logging into [Reown (prev. WalletConnect)](https://reown.com/) and creating a project.
```jsx Web3Providers.tsx theme={null}
"use client";
import "@rainbow-me/rainbowkit/styles.css";
import { getDefaultConfig, RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { WagmiProvider } from "wagmi";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { PropsWithChildren } from "react";
import { aeneid } from "@story-protocol/core-sdk";
const config = getDefaultConfig({
appName: "Test Story App",
projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID as string,
chains: [aeneid],
ssr: true, // If your dApp uses server side rendering (SSR)
});
const queryClient = new QueryClient();
export default function Web3Providers({ children }: PropsWithChildren) {
return (
{children}
);
}
```
```jsx layout.tsx theme={null}
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { PropsWithChildren } from "react";
import Web3Providers from "./Web3Providers";
import { ConnectButton } from "@rainbow-me/rainbowkit";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Example",
description: "This is an Example DApp",
};
export default function RootLayout({ children }: PropsWithChildren) {
return (
{children}
);
}
```
```jsx TestComponent.tsx theme={null}
import { custom, toHex } from 'viem';
import { useWalletClient } from "wagmi";
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
// example of how you would now use the fully setup sdk
export default function TestComponent() {
const { data: wallet } = useWalletClient();
async function setupStoryClient(): Promise {
const config: StoryConfig = {
wallet: wallet,
transport: custom(wallet!.transport),
chainId: "aeneid",
};
const client = StoryClient.newClient(config);
return client;
}
async function registerIp() {
const client = await setupStoryClient();
const response = await client.ipAsset.registerIpAsset({
nft: {
type: 'minted',
nftContract: '0x01...',
tokenId: '1',
}
ipMetadata: {
ipMetadataURI: "test-metadata-uri",
ipMetadataHash: toHex("test-metadata-hash", { size: 32 }),
nftMetadataURI: "test-nft-metadata-uri",
nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }),
}
});
console.log(
`Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}`
);
}
return (
{/* */}
)
}
```
# Reown (WalletConnect) Setup
Source: https://docs.story.foundation/developers/react-guide/setup/reown-setup
Learn how to setup Reown (WalletConnect) in your Story DApp.
**Optional: Official WalletConnect Docs**
Check out the official Wagmi + Reown installation docs [here](https://docs.walletconnect.com/appkit/next/core/installation).
## Install the Dependencies
```bash npm theme={null}
npm install --save @story-protocol/core-sdk @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query
```
```bash pnpm theme={null}
pnpm install @story-protocol/core-sdk viem
```
```bash yarn theme={null}
yarn add @story-protocol/core-sdk viem
```
## Setup
Before diving into the example, make sure you have two things setup:
1. Make sure to have `NEXT_PUBLIC_RPC_PROVIDER_URL` set up in your `.env` file.
* You can use the public default one (`https://aeneid.storyrpc.io`) or any other RPC [here](/network/network-info/aeneid#rpcs).
2. Make sure to have `NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` set up in your `.env` file. Do this by logging into [Reown (prev. WalletConnect)](https://reown.com/) and creating a project.
```jsx config/index.tsx theme={null}
import { cookieStorage, createStorage, http } from "@wagmi/core";
import { WagmiAdapter } from "@reown/appkit-adapter-wagmi";
import { mainnet, arbitrum } from "@reown/appkit/networks";
import { aeneid } from "@story-protocol/core-sdk";
// Get projectId from https://cloud.reown.com
export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
if (!projectId) {
throw new Error("Project ID is not defined");
}
export const networks = [aeneid];
//Set up the Wagmi Adapter (Config)
export const wagmiAdapter = new WagmiAdapter({
storage: createStorage({
storage: cookieStorage,
}),
ssr: true,
projectId,
networks,
});
export const config = wagmiAdapter.wagmiConfig;
```
```jsx context/index.tsx theme={null}
'use client'
import { wagmiAdapter, projectId } from '@/config'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createAppKit } from '@reown/appkit/react'
import { mainnet, arbitrum } from '@reown/appkit/networks'
import React, { type ReactNode } from 'react'
import { cookieToInitialState, WagmiProvider, type Config } from 'wagmi'
// Set up queryClient
const queryClient = new QueryClient()
if (!projectId) {
throw new Error('Project ID is not defined')
}
// Set up metadata
const metadata = {
name: 'appkit-example',
description: 'AppKit Example',
url: 'https://appkitexampleapp.com', // origin must match your domain & subdomain
icons: ['https://avatars.githubusercontent.com/u/179229932']
}
// Create the modal
const modal = createAppKit({
adapters: [wagmiAdapter],
projectId,
networks: [mainnet, arbitrum],
defaultNetwork: mainnet,
metadata: metadata,
features: {
analytics: true // Optional - defaults to your Cloud configuration
}
})
function ContextProvider({ children, cookies }: { children: ReactNode; cookies: string | null }) {
const initialState = cookieToInitialState(wagmiAdapter.wagmiConfig as Config, cookies)
return (
{children}
)
}
export default ContextProvider
```
```jsx app/layout.tsx theme={null}
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
import { headers } from 'next/headers' // added
import ContextProvider from '@/context'
export const metadata: Metadata = {
title: 'AppKit Example App',
description: 'Powered by Reown'
}
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode
}>) {
const headersObj = await headers();
const cookies = headersObj.get('cookie')
return (
{children}
)
}
```
```jsx TestComponent.tsx theme={null}
import { custom, toHex } from 'viem';
import { useWalletClient } from "wagmi";
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
// example of how you would now use the fully setup sdk
export default function TestComponent() {
const { data: wallet } = useWalletClient();
async function setupStoryClient(): Promise {
const config: StoryConfig = {
wallet: wallet,
transport: custom(wallet!.transport),
chainId: "aeneid",
};
const client = StoryClient.newClient(config);
return client;
}
async function registerIp() {
const client = await setupStoryClient();
const response = await client.ipAsset.registerIpAsset({
nft: {
type: 'minted',
nftContract: '0x01...',
tokenId: '1',
}
ipMetadata: {
ipMetadataURI: "test-metadata-uri",
ipMetadataHash: toHex("test-metadata-hash", { size: 32 }),
nftMetadataURI: "test-nft-metadata-uri",
nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }),
}
});
console.log(
`Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}`
);
}
return (
{/* */}
)
}
```
# Tomo Setup
Source: https://docs.story.foundation/developers/react-guide/setup/tomo-setup
Learn how to setup TomoEVMKit in your Story DApp.
**Optional: Official TomoEVMKit Docs**
Check out the official Wagmi + TomoEVMKit installation docs [here](https://docs.tomo.inc/tomo-sdk/tomoevmkit/quick-start).
## Install the Dependencies
```bash npm theme={null}
npm install --save @story-protocol/core-sdk @tomo-inc/tomo-evm-kit wagmi viem @tanstack/react-query
```
```bash pnpm theme={null}
pnpm install @story-protocol/core-sdk viem @tomo-inc/tomo-evm-kit wagmi @tanstack/react-query
```
```bash yarn theme={null}
yarn add @story-protocol/core-sdk viem @tomo-inc/tomo-evm-kit wagmi @tanstack/react-query
```
## Setup
Before diving into the example, make sure you have two things setup:
1. Make sure to have `NEXT_PUBLIC_RPC_PROVIDER_URL` set up in your `.env` file.
* You can use the public default one (`https://aeneid.storyrpc.io`) or any other RPC [here](/network/network-info/aeneid#rpcs).
2. Make sure to have `NEXT_PUBLIC_TOMO_CLIENT_ID` set up in your `.env` file. Do this by logging into the [Tomo Dashboard](https://dashboard.tomo.inc/) and creating a project.
3. Make sure to have `NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` set up in your `.env` file. Do this by logging into [Reown (prev. WalletConnect)](https://reown.com/) and creating a project.
```jsx Web3Providers.tsx theme={null}
"use client";
import '@tomo-inc/tomo-evm-kit/styles.css';
import { getDefaultConfig, TomoEVMKitProvider } from "@tomo-inc/tomo-evm-kit";
import { WagmiProvider } from "wagmi";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { PropsWithChildren } from "react";
import { aeneid } from "@story-protocol/core-sdk";
const config = getDefaultConfig({
appName: "Test Story App",
clientId: process.env.NEXT_PUBLIC_TOMO_CLIENT_ID as string,
projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID as string,
chains: [aeneid],
ssr: true, // If your dApp uses server side rendering (SSR)
});
const queryClient = new QueryClient();
export default function Web3Providers({ children }: PropsWithChildren) {
return (
{children}
);
}
```
```jsx layout.tsx theme={null}
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { PropsWithChildren } from "react";
import Web3Providers from "./Web3Providers";
import { useConnectModal } from "@tomo-inc/tomo-evm-kit";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Example",
description: "This is an Example DApp",
};
export default function RootLayout({ children }: PropsWithChildren) {
const { openConnectModal } = useConnectModal();
return (
{children}
);
}
```
```jsx TestComponent.tsx theme={null}
import { custom, toHex } from 'viem';
import { useWalletClient } from "wagmi";
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
// example of how you would now use the fully setup sdk
export default function TestComponent() {
const { data: wallet } = useWalletClient();
async function setupStoryClient(): Promise {
const config: StoryConfig = {
wallet: wallet,
transport: custom(wallet!.transport),
chainId: "aeneid",
};
const client = StoryClient.newClient(config);
return client;
}
async function registerIp() {
const client = await setupStoryClient();
const response = await client.ipAsset.registerIpAsset({
nft: {
type: 'minted',
nftContract: '0x01...',
tokenId: '1',
}
ipMetadata: {
ipMetadataURI: "test-metadata-uri",
ipMetadataHash: toHex("test-metadata-hash", { size: 32 }),
nftMetadataURI: "test-nft-metadata-uri",
nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }),
}
});
console.log(
`Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}`
);
}
return (
{/* */}
)
}
```
# Using the SDK in React
Source: https://docs.story.foundation/developers/react-guide/using-the-sdk-in-react
Learn how to use the SDK in React once you have it set up.
Once you have the SDK set up in React, you can use it just as we describe in the [TypeScript SDK Guide](/developers/typescript-sdk/overview).
A working code example that shows setting up & calling TypeScript SDK functions in Next.js/React.
View the whole SDK reference, which shows examples and types for every function in our SDK.
## Prerequisites
1. Complete the [SDK setup in React](/developers/react-guide/setup/overview)
## Example
Here is an example of calling an SDK function in React, which will look the same for any function you use:
```jsx TestComponent.tsx theme={null}
import { custom, toHex } from 'viem';
import { useWalletClient } from "wagmi";
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
// example of how you would now use the fully setup sdk
export default function TestComponent() {
const { data: wallet } = useWalletClient();
async function setupStoryClient(): Promise {
const config: StoryConfig = {
wallet: wallet,
transport: custom(wallet!.transport),
chainId: "aeneid",
};
const client = StoryClient.newClient(config);
return client;
}
async function registerIp() {
const client = await setupStoryClient();
const response = await client.ipAsset.registerIpAsset({
nft: {
type: 'mint',
spgNftContract: '0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc',
},
ipMetadata: {
ipMetadataURI: "test-metadata-uri",
ipMetadataHash: toHex("test-metadata-hash", { size: 32 }),
nftMetadataURI: "test-nft-metadata-uri",
nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }),
}
});
console.log(
`Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}`
);
}
return (
{/* */}
)
}
```
# Releases
Source: https://docs.story.foundation/developers/releases
Links to all Story releases
# Smart Contract Guide
Source: https://docs.story.foundation/developers/smart-contracts-guide/overview
For smart contract developers who wish to build on top of Story directly.
In this section, we will briefly go over the protocol contracts and then guide you through how to start building on top of the protocol. If you haven't yet familiarized yourself with the overall architecture, we recommend first going over the [Architecture Overview](/concepts/overview) section.
## Smart Contract Tutorial
Skip the tutorial and view the completed code. Follow the README instructions
to run the tests, or go to the `/test` folder to view all of the example
contracts.
**If you want to set things up from scratch**, then continue with the following tutorials, starting with the [Setup Your Own Project](/developers/smart-contracts-guide/setup) step.
## Our Smart Contracts
As of the current version, our Proof-of-Creativity Protocol is compatible with all EVM chains and is written as a set of Smart Contracts in Solidity. There are two repositories that you may interact with as a developer:
* [Story Protocol Core](https://github.com/storyprotocol/protocol-core-v1) - This repository contains the core protocol logic, consisting of a thin IP registry (the [IP Asset Registry](/concepts/registry/ip-asset-registry)), a set of [Modules](/concepts/modules/overview) defining logic around [Licensing](/concepts/licensing-module/overview), [Royalty](/concepts/royalty-module/overview), [Dispute](/concepts/dispute-module/overview), metadata, and a module manager for administering module and user access control.
* [Story Protocol Periphery](https://github.com/storyprotocol/protocol-periphery-v1)- Whereas the core contracts deal with the underlying protocol logic, the periphery contracts deal with protocol extensions that greatly increase UX and simplify IPA management. This is mostly handled through the [SPG](/concepts/spg/overview).
## Deploy & Verify Contracts on Story
The approach to deploy & verify contracts comes from the [Blockscout official
documentation](https://docs.blockscout.com/developer-support/verifying-a-smart-contract/foundry-verification).
Verify a contract with Blockscout right after deployment (make sure you add "/api/" to the end of the Blockscout homepage explorer URL):
```shell theme={null}
forge create \
--rpc-url \
--private-key $PRIVATE_KEY \
: \
--verify \
--verifier blockscout \
--verifier-url /api/
```
Or if using foundry scripts:
```shell theme={null}
forge script \
--rpc-url \
--private-key $PRIVATE_KEY \
--broadcast \
--verify \
--verifier blockscout \
--verifier-url /api/
```
Do not use RANDAO for pseudo-randomness, instead use onchain VRF (Pyth or Gelato). Currently, RANDAO value is set as the parent block hash and thus is not random for X-1 block.
# Register an IP Asset
Source: https://docs.story.foundation/developers/smart-contracts-guide/register-ip-asset
Learn how to Register an NFT as an IP Asset in Solidity.
Follow the completed code all the way through.
Let's say you have some off-chain IP (ex. a book, a character, a drawing, etc). In order to register that IP on Story, you first need to mint an NFT. This NFT is the **ownership** over the IP. Then you **register** that NFT on Story, turning it into an [IP Asset](/concepts/ip-asset/overview). The below tutorial will walk you through how to do this.
## Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup)
## Before We Start
There are two scenarios:
1. You already have a **custom** ERC-721 NFT contract and can mint from it
2. You want to create an [SPG (Periphery)](/concepts/spg/overview) NFT contract to do minting for you
## Scenario #1: You already have a custom ERC-721 NFT contract and can mint from it
If you already have an NFT minted, or you want to register IP using a custom-built ERC-721 contract, this is the section for you.
As you can see below, the registration process is relatively straightforward. We use `SimpleNFT` as an example, but you can replace it with your own ERC-721 contract.
All you have to do is call `register` on the [IP Asset Registry](/concepts/registry/ip-asset-registry) with:
* `chainid` - you can simply use `block.chainid`
* `tokenContract` - the address of your NFT collection
* `tokenId` - your NFT's ID
Let's create a test file under `test/0_IPARegistrar.t.sol` to see it work and verify the results:
**Contract Addresses**
We have filled in the addresses from the Story contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts)
You can view the `SimpleNFT` contract we're using to test [here](https://github.com/storyprotocol/story-protocol-boilerplate/blob/main/src/mocks/SimpleNFT.sol).
You can view the `SimpleNFT` contract we're using to test [here](https://github.com/storyprotocol/story-protocol-boilerplate/blob/main/src/mocks/SimpleNFT.sol).
```solidity test/0_IPARegistrar.t.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol";
// your own ERC-721 NFT contract
import { SimpleNFT } from "../src/mocks/SimpleNFT.sol";
// Run this test:
// forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/0_IPARegistrar.t.sol
contract IPARegistrarTest is Test {
address internal alice = address(0xa11ce);
// For addresses, see https://docs.story.foundation/developers/deployed-smart-contracts
// Protocol Core - IPAssetRegistry
IIPAssetRegistry internal IP_ASSET_REGISTRY = IIPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b);
SimpleNFT public SIMPLE_NFT;
function setUp() public {
// Create a new Simple NFT collection
SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM");
}
/// @notice Mint an NFT and then register it as an IP Asset.
function test_register() public {
uint256 expectedTokenId = SIMPLE_NFT.nextTokenId();
address expectedIpId = IP_ASSET_REGISTRY.ipId(block.chainid, address(SIMPLE_NFT), expectedTokenId);
uint256 tokenId = SIMPLE_NFT.mint(alice);
address ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId);
assertEq(tokenId, expectedTokenId);
assertEq(ipId, expectedIpId);
assertEq(SIMPLE_NFT.ownerOf(tokenId), alice);
}
}
```
## Scenario #2: You want to create an SPG NFT contract to do minting for you
If you don't have your own custom NFT contract, this is the section for you.
To achieve this, we will be using the [SPG](/concepts/spg/overview), which is a utility contract that allows us to combine multiple transactions into one. In this case, we'll be using the SPG's `mintAndRegisterIp` function which combines both minting an NFT and registering it as an IP Asset.
In order to use `mintAndRegisterIp`, we first have to create a new `SPGNFT` collection. We can do this simply by calling `createCollection` on the `StoryProtocolGateway` contract. Or, if you want to create your own `SPGNFT` for some reason, you can implement the [ISPGNFT](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/interfaces/ISPGNFT.sol) contract interface. Follow the example below to see example parameters you can use to initialize a new SPGNFT.
Once you have your own SPGNFT, all you have to do is call `mintAndRegisterIp` with:
* `spgNftContract` - the address of your SPGNFT contract
* `recipient` - the address of who will receive the NFT and thus be the owner of the newly registered IP. *Note: remember that registering IP on Story is permissionless, so you can register an IP for someone else (by paying for the transaction) yet they can still be the owner of that IP Asset.*
* `ipMetadata` - the metadata associated with your NFT & IP. See [this](/concepts/ip-asset/overview#nft-vs-ip-metadata) section to better understand setting NFT & IP metadata.
1. Run `touch test/0_IPARegistrar.t.sol` to create a test file under `test/0_IPARegistrar.t.sol`. Then, paste in the following code:
**Contract Addresses**
We have filled in the addresses from the Story contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts)
```solidity test/0_IPARegistrar.t.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol";
import { ISPGNFT } from "@storyprotocol/periphery/interfaces/ISPGNFT.sol";
import { IRegistrationWorkflows } from "@storyprotocol/periphery/interfaces/workflows/IRegistrationWorkflows.sol";
import { WorkflowStructs } from "@storyprotocol/periphery/lib/WorkflowStructs.sol";
// Run this test:
// forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/0_IPARegistrar.t.sol
contract IPARegistrarTest is Test {
address internal alice = address(0xa11ce);
// For addresses, see https://docs.story.foundation/developers/deployed-smart-contracts
// Protocol Core - IPAssetRegistry
IIPAssetRegistry internal IP_ASSET_REGISTRY = IIPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b);
// Protocol Periphery - RegistrationWorkflows
IRegistrationWorkflows internal REGISTRATION_WORKFLOWS =
IRegistrationWorkflows(0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424);
ISPGNFT public SPG_NFT;
function setUp() public {
// Create a new NFT collection via SPG
SPG_NFT = ISPGNFT(
REGISTRATION_WORKFLOWS.createCollection(
ISPGNFT.InitParams({
name: "Test Collection",
symbol: "TEST",
baseURI: "",
contractURI: "",
maxSupply: 100,
mintFee: 0,
mintFeeToken: address(0),
mintFeeRecipient: address(this),
owner: address(this),
mintOpen: true,
isPublicMinting: false
})
)
);
}
/// @notice Mint an NFT and register it in the same call via the Story Protocol Gateway.
/// @dev Requires the collection address that is passed into the `mintAndRegisterIp` function
/// to be created via SPG (createCollection), as done above. Or, a contract that
/// implements the `ISPGNFT` interface.
function test_mintAndRegisterIp() public {
uint256 expectedTokenId = SPG_NFT.totalSupply() + 1;
address expectedIpId = IP_ASSET_REGISTRY.ipId(block.chainid, address(SPG_NFT), expectedTokenId);
// Note: The caller of this function must be the owner of the SPG NFT Collection.
// In this case, the owner of the SPG NFT Collection is the contract itself
// because it deployed it in the `setup` function.
// We can make `alice` the recipient of the NFT though, which makes her the
// owner of not only the NFT, but therefore the IP Asset.
(address ipId, uint256 tokenId) = REGISTRATION_WORKFLOWS.mintAndRegisterIp(
address(SPG_NFT),
alice,
WorkflowStructs.IPMetadata({
ipMetadataURI: "https://ipfs.io/ipfs/QmZHfQdFA2cb3ASdmeGS5K6rZjz65osUddYMURDx21bT73",
ipMetadataHash: keccak256(
abi.encodePacked(
"{'title':'My IP Asset','description':'This is a test IP asset','createdAt':'','creators':[]}"
)
),
nftMetadataURI: "https://ipfs.io/ipfs/QmRL5PcK66J1mbtTZSw1nwVqrGxt98onStx6LgeHTDbEey",
nftMetadataHash: keccak256(
abi.encodePacked(
"{'name':'Test NFT','description':'This is a test NFT','image':'https://picsum.photos/200'}"
)
)
}),
true
);
assertEq(ipId, expectedIpId);
assertEq(tokenId, expectedTokenId);
assertEq(SPG_NFT.ownerOf(tokenId), alice);
}
}
```
## Run the Test and Verify the Results
2. Run `forge build`. If everything is successful, the command should successfully compile.
3. Now run the test by executing the following command:
```bash theme={null}
forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/0_IPARegistrar.t.sol
```
## Add License Terms to IP
Congratulations, you registered an IP!
Follow the completed code all the way through.
Now that your IP is registered, you can create and attach [License Terms](/concepts/licensing-module/license-terms) to it. This will allow others to mint a license and use your IP, restricted by the terms.
We will go over this on the next page.
# Setup
Source: https://docs.story.foundation/developers/smart-contracts-guide/setup
Set up your development environment for Story smart contracts.
In this guide, we will show you how to setup the Story smart contract development environment in just a few minutes.
## Prerequisites
* [Install Foundry](https://book.getfoundry.sh/getting-started/installation)
* [Install yarn](https://classic.yarnpkg.com/lang/en/docs/install/)
## Creating a Project
1. Run `foundryup` to automatically install the latest stable version of the precompiled binaries: forge, cast, anvil, and chisel
2. Run the following command in a new directory: `forge init`. This will create a `foundry.toml` and example project files in the project root. By default, forge init will also initialize a new git repository.
3. Initialize a new yarn project: `yarn init`. (⚠️ Note: Only Yarn is compatible with the packages used in this project. Using `npm` or `pnpm` may result in dependency conflicts.)
4. Open up your root-level `foundry.toml` file (located in the top directory of your project) and replace it with this:
```toml theme={null}
[profile.default]
out = 'out'
libs = ['node_modules', 'lib']
cache_path = 'forge-cache'
gas_reports = ["*"]
optimizer = true
optimizer_runs = 20000
test = 'test'
solc = '0.8.26'
fs_permissions = [{ access = 'read', path = './out' }, { access = 'read-write', path = './deploy-out' }]
evm_version = 'cancun'
remappings = [
'@openzeppelin/=node_modules/@openzeppelin/',
'@storyprotocol/core/=node_modules/@story-protocol/protocol-core/contracts/',
'@storyprotocol/periphery/=node_modules/@story-protocol/protocol-periphery/contracts/',
'erc6551/=node_modules/erc6551/',
'forge-std/=node_modules/forge-std/src/',
'ds-test/=node_modules/ds-test/src/',
'@storyprotocol/test/=node_modules/@story-protocol/protocol-core/test/foundry/',
'@solady/=node_modules/solady/'
]
```
5. Remove the example contract files: `rm src/Counter.sol script/Counter.s.sol test/Counter.t.sol`
## Installing Dependencies
Now, we are ready to start installing our dependencies. To incorporate the Story Protocol core and periphery modules, run the following to have them added to your `package.json`. We will also install `openzeppelin` and `erc6551` as a dependency for the contract and test.
```bash theme={null}
# note: you can run them one-by-one, or all at once
yarn add @story-protocol/protocol-core@https://github.com/storyprotocol/protocol-core-v1
yarn add @story-protocol/protocol-periphery@https://github.com/storyprotocol/protocol-periphery-v1
yarn add @openzeppelin/contracts
yarn add @openzeppelin/contracts-upgradeable
yarn add erc6551
yarn add solady
```
Additionally, for working with Foundry's test kit, we also recommend adding the following `devDependencies`:
```bash theme={null}
yarn add -D https://github.com/dapphub/ds-test
yarn add -D github:foundry-rs/forge-std#v1.7.6
```
Now we are ready to build a simple test registration contract!
# Attach Terms to an IPA
Source: https://docs.story.foundation/developers/typescript-sdk/attach-terms
Learn how to Attach License Terms to an IP Asset in TypeScript.
This section demonstrates how to attach [License Terms](/concepts/licensing-module/license-terms) to an [IP Asset](/concepts/ip-asset). By attaching terms, users can publicly mint [License Tokens](/concepts/licensing-module/license-token) (the on-chain "license") with those terms from the IP.
### Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup)
## 1. Before We Start
We should mention that you do not need an existing IP Asset to attach terms to it. As we saw in the previous section, you can register an IP Asset and attach terms to it in the same transaction.
## 2. Register License Terms
In order to attach terms to an IP Asset, let's first create them!
[License Terms](/concepts/licensing-module/license-terms) are a configurable set of values that define restrictions on licenses minted from your IP that have those terms. For example, "If you mint this license, you must share 50% of your revenue with me." You can view the full set of terms in [PIL Terms](/concepts/programmable-ip-license/pil-terms).
If License Terms already exist on our protocol for the identical set of parameters you intend to create, it is unnecessary to create it again and the function will simply return the existing `licenseTermsId` and an undefined `txHash`. License Terms are protocol-wide, so you can use existing License Terms by its `licenseTermsId`.
Below is a code example showing how to create new terms:
Associated Docs:
[license.registerPILTerms](/sdk-reference/license#registerpilterms)
```typescript main.ts theme={null}
import { LicenseTerms } from "@story-protocol/core-sdk";
import { zeroAddress } from "viem";
// you should already have a client set up (prerequisite)
import { client } from "./utils";
async function main() {
const licenseTerms: LicenseTerms = {
defaultMintingFee: 0n,
// must be a whitelisted revenue token from https://docs.story.foundation/developers/deployed-smart-contracts
// in this case, we use $WIP
currency: "0x1514000000000000000000000000000000000000",
// RoyaltyPolicyLAP address from https://docs.story.foundation/developers/deployed-smart-contracts
royaltyPolicy: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E",
transferable: false,
expiration: 0n,
commercialUse: false,
commercialAttribution: false,
commercializerChecker: zeroAddress,
commercializerCheckerData: "0x",
commercialRevShare: 0,
commercialRevCeiling: 0n,
derivativesAllowed: false,
derivativesAttribution: false,
derivativesApproval: false,
derivativesReciprocal: false,
derivativeRevCeiling: 0n,
uri: "",
};
const response = await client.license.registerPILTerms({
...licenseTerms,
});
console.log(
`PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}`
);
}
main();
```
### 2a. PIL Flavors
As you see above, you have to choose between a lot of terms.
We have convenience functions to help you register new terms. We have created [PIL Flavors](/concepts/programmable-ip-license/pil-flavors), which are pre-configured popular combinations of License Terms to help you decide what terms to use. You can view those PIL Flavors and then register terms using the following convenience functions:
Free remixing with attribution. No commercialization.
Pay to use the license with attribution, but don't have to share revenue.
Pay to use the license with attribution and pay % of revenue earned.
Free remixing and commercial use with attribution.
You can easily register a flavor of terms like so:
```typescript main.ts theme={null}
import { PILFlavor, WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
import { parseEther } from "viem";
// you should already have a client set up (prerequisite)
import { client } from "./utils";
async function main() {
const response = await client.license.registerPILTerms(
PILFlavor.commercialRemix({
commercialRevShare: 5,
defaultMintingFee: parseEther("1"), // 1 $IP
currency: WIP_TOKEN_ADDRESS,
})
);
console.log(
`PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}`
);
}
main();
```
## 3. Attach License Terms
Now that we have created terms and have the associated `licenseTermsId`, we can attach them to an existing IP Asset like so:
Associated Docs:
[license.attachLicenseTerms](/sdk-reference/license#attachlicenseterms)
```typescript main.ts theme={null}
import { LicenseTerms } from "@story-protocol/core-sdk";
import { zeroAddress } from "viem";
// you should already have a client set up (prerequisite)
import { client } from "./utils";
async function main() {
// previous code here ...
const response = await client.license.attachLicenseTerms({
// insert your newly created license terms id here
licenseTermsId: LICENSE_TERMS_ID,
// insert the ipId you want to attach terms to here
ipId: "0x4c1f8c1035a8cE379dd4ed666758Fb29696CF721",
});
if (response.success) {
console.log(
`Attached License Terms to IPA at transaction hash ${response.txHash}.`
);
} else {
console.log(`License Terms already attached to this IPA.`);
}
}
main();
```
## 3. Mint a License
Now that we have attached License Terms to our IP, the next step is minting a License Token, which we'll go over on the next page.
# Claim Revenue
Source: https://docs.story.foundation/developers/typescript-sdk/claim-revenue
Learn how to claim due revenue from a child IP Asset in TypeScript.
All of this page is covered in this working code example.
This section demonstrates how to claim due revenue from an IP Asset.
There are two main ways revenue can be claimed:
1. **Scenario #1**: Someone pays my IP Asset directly, and I claim that revenue.
2. **Scenario #2**: Someone pays a derivative IP Asset of my IP, and I have the right to a % of their revenue based on the `commercialRevShare` in the license terms.
### Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup)
2. Have a basic understanding of the [Royalty Module](/concepts/royalty-module)
3. Obviously, there must be a payment to be claimed. Read [Pay an IPA](/developers/typescript-sdk/pay-ipa)
## Before We Start
When payments are made, they eventually end up in an IP Asset's [IP Royalty Vault](/concepts/royalty-module/ip-royalty-vault). From here, they are claimed/transferred to whoever owns the Royalty Tokens associated with it, which represent a % of revenue share for a given IP Asset's IP Royalty Vault.
The IP Account (the smart contract that represents the [IP Asset](/concepts/ip-asset)) is what holds 100% of the Royalty Tokens when it's first registered. So usually, it indeed holds most of the Royalty Tokens.
**Quick Note**. The below scenarios and examples use a [Liquid Absolute Percentage](/concepts/royalty-module/liquid-absolute-percentage) royalty policy. This is currently one of two royalty policies you can use.
## Scenario #1
In this scenario, I own IP Asset 3. Someone pays my IP Asset 3 directly, and I claim that revenue. Let's view this in steps:
1. As we can see in the below diagram, when someone pays IP Asset 3 100 \$WIP, 85 \$WIP automatically gets deposited into IP Royalty Vault 3 (based on the license terms, which specifiies a LAP royalty policy and the resulting royalty stack).
2. Now, IP Asset 3 wants to claim its revenue sitting in the IP Royalty Vault 3. It will look like this:
Below is how IP Asset 3 would claim their revenue, as shown in the image above, with the SDK:
Associated Docs:
[royalty.claimAllRevenue](/sdk-reference/royalty#claimallrevenue)
Claiming revenue is permissionless. Any wallet can run the claim revenue transaction for an IP.
```typescript main.ts theme={null}
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
// you should already have a client set up (prerequisite)
import { client } from "./client";
async function main() {
const claimRevenue = await client.royalty.claimAllRevenue({
// IP Asset 3's ipId
ancestorIpId: "0x03",
// the address that owns the royalty tokens,
// which is the IP Account in this case
claimer: "0x03",
currencyTokens: [WIP_TOKEN_ADDRESS],
childIpIds: [],
royaltyPolicies: [],
claimOptions: {
// If the wallet claiming the revenue is the owner of the
// IP Account/IP Asset (in other words, the owner of the
// IP's underlying NFT), `claimAllRevenue` will transfer all
// earnings to the user's external wallet holding the NFT
// instead of the IP Account, for convenience. You can disable it here.
autoTransferAllClaimedTokensFromIp: true,
// Unwraps the claimed $WIP to $IP for you
autoUnwrapIpTokens: true,
},
});
console.log(`Claimed revenue: ${claimRevenue.claimedTokens}`);
}
main();
```
## Scenario #2
In this scenario, I own IP Asset 1. Someone pays a derivative IP Asset 3, and I have the right to a % of their revenue based on the `commercialRevShare` in the license terms. This is exactly the same as Scenario #1, except one extra step is added. Let's view this in steps:
1. As we can see in the below diagram, when someone pays IP Asset 3 100 \$WIP, 15 \$WIP automatically gets deposited to the LAP royalty policy contract to be distributed to ancestors.
2. Then, in a second step, the tokens are transferred to the ancestors' [IP Royalty Vault](/concepts/royalty-module/ip-royalty-vault) based on the negotiated `commercialRevShare` in the license terms.
3. Lastly, IP Asset 1 wants to claim their revenue sitting in its associated IP Royalty Vault. It will look like this:
Below is how IP Asset 1 (or 2) would claim their revenue, as shown in the image above, with the SDK:
Associated Docs:
[royalty.claimAllRevenue](/sdk-reference/royalty#claimallrevenue)
Claiming revenue is permissionless. Any wallet can run the claim revenue transaction for an IP.
```typescript main.ts theme={null}
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
// you should already have a client set up (prerequisite)
import { client } from "./client";
async function main() {
const claimRevenue = await client.royalty.claimAllRevenue({
// IP Asset 1's ipId
ancestorIpId: "0x01",
// the address that owns the royalty tokens,
// which is the IP Account in this case
claimer: "0x01",
currencyTokens: [WIP_TOKEN_ADDRESS],
// IP Asset 3's ipId
childIpIds: ["0x03"],
// Aeneid testnet address of RoyaltyPolicyLAP
royaltyPolicies: ["0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E"],
claimOptions: {
// If the wallet claiming the revenue is the owner of the
// IP Account/IP Asset (in other words, the owner of the
// IP's underlying NFT), `claimAllRevenue` will transfer all
// earnings to the user's external wallet holding the NFT
// instead of the IP Account, for convenience. You can disable it here.
autoTransferAllClaimedTokensFromIp: true,
// Unwraps the claimed $WIP to $IP for you
autoUnwrapIpTokens: true,
},
});
console.log(`Claimed revenue: ${claimRevenue.claimedTokens}`);
}
main();
```
## Scenario #3
In this scenario, I own IP Asset 1. Someone pays a derivative IP Asset 3, and I have the right to a % of their revenue based on the `commercialRevShare` in the license terms. The difference here is that I have previously transferred the royalty tokens in the IP Account to an external wallet, most commonly the wallet that owns the IP. This is exactly the same as Scenario #2, except royalty is being claimed to an external wallet instead of the IP Account. Let's view this in steps:
1. As we can see in the below diagram, when someone pays IP Asset 3 100 \$WIP, 15 \$WIP automatically gets deposited to the LAP royalty policy contract to be distributed to ancestors.
2. Then, in a second step, the tokens are transferred to the ancestors' [IP Royalty Vault](/concepts/royalty-module/ip-royalty-vault) based on the negotiated `commercialRevShare` in the license terms.
3. Lastly, IP Asset 1 wants to claim their revenue sitting in its associated IP Royalty Vault. It will look like this:
Below is how IP Asset 1 (or 2) would claim their revenue, as shown in the image above, with the SDK:
Associated Docs:
[royalty.claimAllRevenue](/sdk-reference/royalty#claimallrevenue)
Claiming revenue is permissionless. Any wallet can run the claim revenue transaction for an IP.
```typescript main.ts theme={null}
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
// you should already have a client set up (prerequisite)
import { client } from "./client";
async function main() {
// transfer the royalty tokens from the
// ip account to the external wallet
// NOTE: this can only be called by the IP owner
// and only needs to be called once. Any future
// claims will be to this external wallet
const royaltyVaultAddress = await client.royalty.getRoyaltyVaultAddress("0x01");
const transferRoyaltyTokens = await client.ipAccount.transferErc20({
ipId: "0x01",
tokens: [
{
address: royaltyVaultAddress,
amount: 100_000_000, // 100% of the royalty tokens
target: "0x04", // the external wallet
},
],
});
// claim the revenue to the external wallet
const claimRevenue = await client.royalty.claimAllRevenue({
// IP Asset 1's ipId
ancestorIpId: "0x01",
// the address that owns the royalty tokens,
// which is the external wallet in this case
claimer: "0x04",
currencyTokens: [WIP_TOKEN_ADDRESS],
// IP Asset 3's ipId
childIpIds: ["0x03"],
// Aeneid testnet address of RoyaltyPolicyLAP
royaltyPolicies: ["0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E"],
claimOptions: {
// If the wallet claiming the revenue is the owner of the
// IP Account/IP Asset (in other words, the owner of the
// IP's underlying NFT), `claimAllRevenue` will transfer all
// earnings to the user's external wallet holding the NFT
// instead of the IP Account, for convenience. You can disable it here.
autoTransferAllClaimedTokensFromIp: true,
// Unwraps the claimed $WIP to $IP for you
autoUnwrapIpTokens: true,
},
});
console.log(`Claimed revenue: ${claimRevenue.claimedTokens}`);
}
main();
```
## View Completed Code
Congratulations, you claimed revenue using the [Royalty Module](/concepts/royalty-module)!
All of this page is covered in this working code example.
## Dispute an IP
Now what happens if an IP Asset doesn't pay their due share? We can dispute the IP on-chain, which we will cover on the next page.
# Mint a License Token
Source: https://docs.story.foundation/developers/typescript-sdk/mint-license
Learn how to mint a License Token from an IP Asset in TypeScript.
This section demonstrates how to mint a [License Token](/concepts/licensing-module/license-token) from an [IP Asset](/concepts/ip-asset). You can only mint a License Token from an IP Asset if the IP Asset has [License Terms](/concepts/licensing-module/license-terms) attached to it. A License Token is minted as an ERC-721.
There are two reasons you'd mint a License Token:
1. To hold the license and be able to use the underlying IP Asset as the license described (for ex. "Can use commercially as long as you provide proper attribution and share 5% of your revenue)
2. Use the license token to link another IP Asset as a derivative of it. *Note though that, as you'll see later, some SDK functions don't require you to explicitly mint a license token first in order to register a derivative, and will actually handle it for you behind the scenes.*
### Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup)
2. An IP Asset that has License Terms added. Learn how to add License Terms to an IPA [here](/developers/typescript-sdk/attach-terms).
## 1. Mint License
Let's say that IP Asset (`ipId = 0x01`) has License Terms (`licenseTermdId = 10`) attached to it. We want to mint 2 License Tokens with those terms to a specific wallet address (`0x02`).
Be mindful that some IP Assets may have license terms attached that require the user minting the license to pay a `defaultMintingFee`. You can see an example of that in the [TypeScript Tutorial](https://github.com/storyprotocol/typescript-tutorial/blob/main/scripts/derivative/registerDerivativeCommercial.ts).
Note that a license token can only be minted if the `licenseTermsId` are already attached to the IP Asset, making it a publicly available license. The IP owner can, however, mint a [private license](/concepts/licensing-module/license-token#private-licenses) by minting a license token with a `licenseTermsId` that is not attached to the IP Asset.
Associated Docs:
[license.mintLicenseTokens](/sdk-reference/license#mintlicensetokens)
```typescript main.ts theme={null}
// you should already have a client set up (prerequisite)
import { client } from "./client";
async function main() {
const response = await client.license.mintLicenseTokens({
licenseTermsId: "10",
licensorIpId: "0x641E638e8FCA4d4844F509630B34c9D524d40BE5",
receiver: "0x641E638e8FCA4d4844F509630B34c9D524d40BE5", // optional. if not provided, it will go to the tx sender
amount: 2,
maxMintingFee: BigInt(0), // disabled
maxRevenueShare: 100, // default
});
console.log(
`License Token minted at transaction hash ${response.txHash}, License IDs: ${response.licenseTokenIds}`
);
}
main();
```
### 1a. Setting Restrictions on Minting License Token
This is a note for owners of an IP Asset who want to set restrictions on who or how their license tokens are minted. You can:
* Set a max number of licenses that can be minted
* Charge dynamic fees based on who / how many are minted
* Whitelisted certain wallets to mint the tokens
... and more. Learn more by checking out the [License Config](/concepts/licensing-module/license-config) section of our documentation.
## 2. Register a Derivative
Now that we have minted a License Token, we can hold it or use it to link an IP Asset as a derivative. We will go over that on the next page.
*Note though that, as you'll see later, some SDK functions don't require you to explicitly mint a license token first in order to register a derivative, and will actually handle it for you behind the scenes.*
### 2a. Why would I ever use a License Token if it's not needed?
There are a few times when **you would need** a License Token to register a derivative:
* The License Token contains private license terms, so you would only be able to register as a derivative if you had the License Token that was manually minted by the owner. More on that [here](/concepts/licensing-module/license-token#private-licenses).
* The License Token (which is an NFT) costs a `mintingFee` to mint, and you were able to buy it on a marketplace for a cheaper price. Then it makes more sense to simply register with the License Token then have to pay the more expensive `defaultMintingFee`.
# Overview
Source: https://docs.story.foundation/developers/typescript-sdk/overview
For TypeScript developers who want to build with Story.
The best way to get started is to get your hands dirty and start building.
Extremely easy & straightforward working code examples for all of the following tutorials.
View the whole SDK reference, which shows examples and types for every function in our SDK.
In the following series of tutorials, you will learn how to build IP applications with the Story SDK along with the concepts we mentioned in the [Architecture Overview](/concepts/overview).
# Pay an IPA
Source: https://docs.story.foundation/developers/typescript-sdk/pay-ipa
Learn how to pay an IP Asset in TypeScript.
All of this page is covered in this working code example.
This section demonstrates how to pay an IP Asset. There are a few reasons you would do this:
1. You simply want to "tip" an IP
2. You have to because your license terms with an ancestor IP require you to forward a certain % of payment
In either scenario, you would use the below `payRoyaltyOnBehalf` function. When this happens, the [Royalty Module](/concepts/royalty-module) automatically handles the different payment flows such that parent IP Assets who have negotiated a certain `commercialRevShare` with the IPA being paid can claim their due share.
### Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup)
2. Have a basic understanding of the [Royalty Module](/concepts/royalty-module)
## Before We Start
You can pay an IP Asset using the `payRoyaltyOnBehalf` function.
You will be paying the IP Asset with [\$WIP](https://aeneid.storyscan.io/address/0x1514000000000000000000000000000000000000). **Note that if you don't have enough \$WIP, the function will auto wrap an equivalent amount of \$IP into \$WIP for you.** If you don't have enough of either, it will fail.
To help with the following scenarios, let's say we have a parent IP Asset that has negotiated a 50% `commercialRevShare` with its child IP Asset.
### Whitelisted Revenue Tokens
Only tokens that are whitelisted by our protocol can be used as payment ("revenue") tokens. \$WIP is one of those tokens. To see that list, go [here](/developers/deployed-smart-contracts#whitelisted-revenue-tokens).
If you want to test paying IP Assets, you'll probably want a whitelisted revenue token you can mint freely for testing. We have provided [MockERC20](https://aeneid.storyscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E?tab=write_contract#0x40c10f19) on Aeneid testnet which you can mint and pay with. Then when you're ready, you should use \$WIP.
## Scenario #1: Tipping an IP Asset
In this scenario, you're an external 3rd-party user who wants to pay an IP Asset 2 \$WIP for being cool. When you call the function below, you should make `payerIpId` a zero address because you are not paying on behalf of an IP Asset. Additionally, you would set `amount` to 2.
Associated Docs:
[royalty.payRoyaltyOnBehalf](/sdk-reference/royalty#payroyaltyonbehalf)
```typescript main.ts theme={null}
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
// you should already have a client set up (prerequisite)
import { client } from "./utils";
import { zeroAddress, parseEther } from "viem";
async function main() {
const payRoyalty = await client.royalty.payRoyaltyOnBehalf({
receiverIpId: "0x0b825D9E5FA196e6B563C0a446e8D9885057f9B1", // the ip you're paying
payerIpId: zeroAddress,
token: WIP_TOKEN_ADDRESS,
amount: parseEther("2"), // 2 $WIP
});
console.log(`Paid royalty at transaction hash ${payRoyalty.txHash}`);
}
main();
```
Let's say the IP Asset you're paying is a derivative. And due to existing license terms with a parent that specify 50% `commercialRevShare`, 50% of the revenue (2\*0.5 = 1) would automatically be claimable by the parent thanks to the [Royalty Module](/concepts/royalty-module), such that both the parent and child IP Assets earn 1 \$WIP. We'll go over this on the next page.
## Scenario #2: Paying Due Share
In this scenario, lets say a derivative IP Asset earned 2 USD off-chain. Because the derivative owes the parent IP Asset 50% of its revenue, it could give the parent 1 USD off-chain and be ok. Or, it can send 1 \$USD equivalent to the parent on-chain *(for this example, let's just assume 1 \$WIP = 1 USD)*.
Associated Docs:
[royalty.payRoyaltyOnBehalf](/sdk-reference/royalty#payroyaltyonbehalf)
```typescript main.ts theme={null}
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
// you should already have a client set up (prerequisite)
import { client } from "./utils";
import { parseEther } from "viem";
async function main() {
const payRoyalty = await client.royalty.payRoyaltyOnBehalf({
receiverIpId: "0xDa03c4B278AD44f5a669e9b73580F91AeDE0E3B2", // parentIpId
payerIpId: "0x0b825D9E5FA196e6B563C0a446e8D9885057f9B1", // childIpId
token: WIP_TOKEN_ADDRESS,
amount: parseEther("1"), // 1 $WIP
});
console.log(`Paid royalty at transaction hash ${payRoyalty.txHash}`);
}
main();
```
### Complex Royalty Graphs
Let's say the child earned 1,000 USD off-chain, and is linked to a huge ancestor tree where each parent has a different set of complex license terms. In this scenario, you won't be able to individually calculate each payment to each parent. Instead, you would just pay *yourself* the amount you earned, and the [Royalty Module](/concepts/royalty-module) will automate the payment, such that each ancestor gets their due share.
## View Completed Code
Congratulations, you paid an IP Asset on-chain!
All of this page is covered in this working code example.
## Claiming Revenue
Now that we have paid revenue, we need to learn how to claim it! We will cover that on the next page.
# Raise a Dispute
Source: https://docs.story.foundation/developers/typescript-sdk/raise-dispute
Learn how to create an on-chain dispute in TypeScript.
All of this page is covered in this working code example.
This section demonstrates how to dispute an IP on Story. There are many instances where you may want to dispute an IP - whether that IP is or is not owned by you. Disputing IP on Story is easy thanks to our [Dispute Module](/concepts/dispute-module) and the [UMA Arbitration Policy](/concepts/dispute-module/uma-arbitration-policy).
Let's say you register a drawing, and then someone else registers that drawing with 1 pixel off. You can dispute it along a `IMPROPER_REGISTRATION` tag, which communicates potential plagiarism.
In this tutorial, you will simply learn how to flag an IP as being disputed.
### Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup)
2. Have a basic understanding of the [Dispute Module](/concepts/dispute-module)
## 1. Dispute an IP
To dispute an IP Asset, you will need:
* The `targetIpId` of the IP Asset you are disputing (we use a test one below)
* The `targetTag` that you are applying to the dispute. Only [whitelisted tags](/concepts/dispute-module/overview#dispute-tags) can be applied.
* A `cid` (Content Identifier) is a unique identifier in IPFS that represents the dispute evidence you must provide, as described [here](/concepts/dispute-module/uma-arbitration-policy#dispute-evidence-submission-guidelines) (we use a test one below).
**Note you can only provide a CID one time.** After it is used, it can't be
used as evidence again.
Create a `main.ts` file and add the code below:
```typescript main.ts theme={null}
import { client } from "./utils";
import { parseEther } from "viem";
import { DisputeTargetTag } from "@story-protocol/core-sdk";
async function main() {
const disputeResponse = await client.dispute.raiseDispute({
targetIpId: "0x6b42d065aDCDA6fA83B59ad731841360dC5321fB",
// NOTE: you must use your own CID here, because every time it is used,
// the protocol does not allow you to use it again
cid: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR",
// you must pick from one of the whitelisted tags here: https://docs.story.foundation/concepts/dispute-module#dispute-tags
targetTag: DisputeTargetTag.IMPROPER_REGISTRATION,
bond: parseEther("0.1"), // minimum of 0.1
liveness: 2592000,
});
console.log(
`Dispute raised at transaction hash ${disputeResponse.txHash}, Dispute ID: ${disputeResponse.disputeId}`
);
}
main();
```
## 2. View Completed Code
See a completed, working example disputing an IP.
# Register a Derivative
Source: https://docs.story.foundation/developers/typescript-sdk/register-derivative
Learn how to register a derivative/remix IP Asset as a child of another in TypeScript.
All of this page is covered in this working code example.
This section demonstrates how to register an IP Asset as a derivative of another.
### Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup)
## 1. Before We Start
Registering a derivative IP Asset requires you have a License Token from the parent IP Assets you plan to be a derivative of. If you don't, the SDK will handle it for you.
### 1a. Why would I ever use a License Token if it's not needed?
There are a few times when **you would need** a License Token to register a derivative:
* The License Token contains private license terms, so you would only be able to register as a derivative if you had the License Token that was manually minted by the owner. More on that [here](/concepts/licensing-module/license-token#private-licenses).
* The License Token (which is an NFT) costs a `mintingFee` to mint, and you were able to buy it on a marketplace for a cheaper price. Then it makes more sense to simply register with the License Token then have to pay the more expensive `defaultMintingFee`.
## 2. Register Derivative
In this example we're going to assume you have no license tokens, the child IP is not yet registered, and you don't have your own NFT contract or an already minted NFT.
Associated Docs:
[ipAsset.registerDerivativeIpAsset](/sdk-reference/ipasset#registerderivativeipasset)
```typescript main.ts theme={null}
import { IpMetadata, DerivativeData } from "@story-protocol/core-sdk";
import { client } from "./utils";
import { uploadJSONToIPFS } from "./uploadToIpfs";
import { createHash } from "crypto";
import { Address } from "viem";
async function main() {
// previous code here ...
const derivData = {
// TODO: insert the parent's ipId
parentIpIds: [PARENT_IP_ID],
// TODO: insert the licenseTermsId attached to parent IpId
licenseTermsIds: [LICENSE_TERMS_ID],
};
const response = await client.ipAsset.registerDerivativeIpAsset({
nft: {
type: "mint",
spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc",
},
derivData,
ipMetadata: {
ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`,
ipMetadataHash: `0x${ipHash}`,
nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`,
nftMetadataHash: `0x${nftHash}`,
},
});
console.log(
`Completed at transaction hash ${response.txHash}, IPA ID: ${response.ipId}, Token ID: ${response.tokenId}`
);
}
```
## 3. View Completed Code
Congratulations, you registered a derivative IP Asset!
All of this page is covered in this working code example.
## 4. Paying and Claiming Revenue
Now that we have established parent-child IP relationships, we can begin to explore payments and automated revenue share based on the license terms. We'll cover that in the upcoming pages.
# Register an IP Asset
Source: https://docs.story.foundation/developers/typescript-sdk/register-ip-asset
Learn how to Register an NFT as an IP Asset in TypeScript.
Follow the completed code all the way through.
Let's say you have some off-chain IP (ex. a book, a character, a drawing, etc). In order to register that IP on Story, you first need to mint an NFT. This NFT is the **ownership** over the IP. Then you **register** that NFT on Story, turning it into an [IP Asset](/concepts/ip-asset). The below tutorial will walk you through how to do this.
### Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup)
2. \[OPTIONAL] Go to [Pinata](https://pinata.cloud/) and create a new API key. Add the JWT to your `.env` file:
```text .env theme={null}
PINATA_JWT=
```
3. \[OPTIONAL] Install the `pinata-web3` dependency:
```bash Terminal theme={null}
npm install pinata-web3
```
## 1. Set up your IP Metadata
We can set metadata on our NFT & IP, *but you don't have to*. To do this, view the [IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard) and construct your metadata for both your NFT & IP.
```typescript main.ts theme={null}
// you should already have a client set up (prerequisite)
import { client } from "./utils";
async function main() {
const ipMetadata = {
title: "Ippy",
description: "Official mascot of Story.",
image:
"https://ipfs.io/ipfs/QmSamy4zqP91X42k6wS7kLJQVzuYJuW2EN94couPaq82A8",
imageHash:
"0x21937ba9d821cb0306c7f1a1a2cc5a257509f228ea6abccc9af1a67dd754af6e",
mediaUrl:
"https://ipfs.io/ipfs/QmSamy4zqP91X42k6wS7kLJQVzuYJuW2EN94couPaq82A8",
mediaHash:
"0x21937ba9d821cb0306c7f1a1a2cc5a257509f228ea6abccc9af1a67dd754af6e",
mediaType: "image/png",
creators: [
{
name: "Story Foundation",
address: "0x67ee74EE04A0E6d14Ca6C27428B27F3EFd5CD084",
description: "The World's IP Blockchain",
contributionPercent: 100,
socialMedia: [
{
platform: "Twitter",
url: "https://twitter.com/storyprotocol",
},
{
platform: "Website",
url: "https://story.foundation",
},
],
},
],
};
}
main();
```
## 2. Set up your NFT Metadata
The NFT Metadata follows the [ERC-721 Metadata Standard](https://eips.ethereum.org/EIPS/eip-721).
```typescript main.ts theme={null}
import { IpMetadata } from "@story-protocol/core-sdk";
import { client } from "./utils";
async function main() {
// previous code here ...
const nftMetadata = {
name: "Ownership NFT",
description: "This is an NFT representing owernship of our IP Asset.",
image: "https://picsum.photos/200",
};
}
main();
```
## 3. Upload your IP and NFT Metadata to IPFS
In a separate `uploadToIpfs` file, create a function to upload your IP & NFT Metadata objects to IPFS:
```typescript uploadToIpfs.ts theme={null}
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
});
export async function uploadJSONToIPFS(jsonMetadata: any): Promise {
const { IpfsHash } = await pinata.upload.json(jsonMetadata);
return IpfsHash;
}
```
You can then use that function to upload your metadata, as shown below:
```typescript main.ts theme={null}
import { IpMetadata } from "@story-protocol/core-sdk";
import { client } from "./utils";
import { uploadJSONToIPFS } from "./uploadToIpfs";
import { createHash } from "crypto";
async function main() {
// previous code here ...
const ipIpfsHash = await uploadJSONToIPFS(ipMetadata);
const ipHash = createHash("sha256")
.update(JSON.stringify(ipMetadata))
.digest("hex");
const nftIpfsHash = await uploadJSONToIPFS(nftMetadata);
const nftHash = createHash("sha256")
.update(JSON.stringify(nftMetadata))
.digest("hex");
}
main();
```
## 4. Register an NFT as an IP Asset
Remember that in order to register a new IP, we first have to mint an NFT, which will represent the underlying ownership of the IP. This NFT then gets "registered" and becomes an [IP Asset](/concepts/ip-asset).
Luckily, we can use the `registerIpAsset` function to mint an NFT and register it as an IP Asset in the same transaction.
This function needs an SPG NFT Contract to mint from.
### 4a. What SPG NFT contract address should I use?
For simplicity, you can use a public collection we have created for you on Aeneid testnet: `0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc`. On Mainnet, or even when testing a real scenario on Aeneid, you should **create your own** contract as described in the "Using a custom ERC-721 contract" section below.
Using a public collection we provide for you is fine, but when you do this for real, you should make your own NFT Collection for your IPs. You can do this in 2 ways:
1. Deploy a contract that implements the [ISPGNFT](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/interfaces/ISPGNFT.sol) interface, or use the SDK's [createNFTCollection](/sdk-reference/nftclient#createnftcollection) function (shown below) to do it for you. This will give you your own SPG NFT Collection that only you can mint from.
```typescript createSpgNftCollection.ts theme={null}
import { zeroAddress } from "viem";
import { client } from "./utils";
async function createSpgNftCollection() {
const newCollection = await client.nftClient.createNFTCollection({
name: "Test NFTs",
symbol: "TEST",
isPublicMinting: false,
mintOpen: true,
mintFeeRecipient: zeroAddress,
contractURI: "",
});
console.log("New collection created:", {
"SPG NFT Contract Address": newCollection.spgNftContract,
"Transaction Hash": newCollection.txHash,
});
}
createSpgNftCollection();
```
2. Create a custom ERC-721 NFT collection on your own. See a working code example [here](https://github.com/storyprotocol/typescript-tutorial/blob/main/scripts/registration/registerCustom.ts). This is helpful if you **already have a custom NFT contract that has your own custom logic, or if your IPs themselves are NFTs.**
Here is the code to register an IP:
Associated Docs:
[ipAsset.registerIpAsset](/sdk-reference/ipasset#registeripasset)
```typescript main.ts theme={null}
import { IpMetadata } from "@story-protocol/core-sdk";
import { client } from "./utils";
import { uploadJSONToIPFS } from "./uploadToIpfs";
import { createHash } from "crypto";
import { Address } from "viem";
async function main() {
// previous code here ...
const response = await client.ipAsset.registerIpAsset({
nft: {
type: "mint",
spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc",
},
ipMetadata: {
ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`,
ipMetadataHash: `0x${ipHash}`,
nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`,
nftMetadataHash: `0x${nftHash}`,
},
});
console.log(
`Root IPA created at transaction hash ${response.txHash}, IPA ID: ${response.ipId}`
);
console.log(
`View on the explorer: https://aeneid.explorer.story.foundation/ipa/${response.ipId}`
);
}
main();
```
## 5. Add License Terms to IP
During the registration process, you can attach [License Terms](/concepts/licensing-module/license-terms) to the IP. This will allow others to mint a license and use your IP, restricted by the terms.
```typescript main.ts theme={null}
import {
IpMetadata,
PILFlavor,
WIP_TOKEN_ADDRESS,
} from "@story-protocol/core-sdk";
import { client } from "./utils";
import { uploadJSONToIPFS } from "./uploadToIpfs";
import { createHash } from "crypto";
import { Address, parseEther } from "viem";
async function main() {
// previous code here ...
const response = await client.ipAsset.registerIpAsset({
nft: {
type: "mint",
spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc",
},
// [!code ++:9]
licenseTermsData: [
{
terms: PILFlavor.commercialRemix({
commercialRevShare: 5,
defaultMintingFee: parseEther("1"), // 1 $IP
currency: WIP_TOKEN_ADDRESS,
}),
},
],
ipMetadata: {
ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`,
ipMetadataHash: `0x${ipHash}`,
nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`,
nftMetadataHash: `0x${nftHash}`,
},
});
console.log(
`Root IPA created at transaction hash ${response.txHash}, IPA ID: ${response.ipId}`
);
console.log(
`View on the explorer: https://aeneid.explorer.story.foundation/ipa/${response.ipId}`
);
}
main();
```
## 6. View Completed Code
Congratulations, you registered an IP and attached license terms to it!
Follow the completed code all the way through.
# Setup Client
Source: https://docs.story.foundation/developers/typescript-sdk/setup
Learn how to setup the TypeScript SDK.
### Prerequisites
We require node version 18 or later version and npm version 8 to be installed in your environment. To install node and npm, we recommend you go to the [Node.js official website](https://nodejs.org) and download the latest LTS (Long Term Support) version.
### Install the Dependencies
Install the [Story SDK](https://www.npmjs.com/package/@story-protocol/core-sdk) node package, as well as [viem](https://www.npmjs.com/package/viem).
```bash npm theme={null}
npm install --save @story-protocol/core-sdk viem
```
```bash pnpm theme={null}
pnpm install @story-protocol/core-sdk viem
```
```bash yarn theme={null}
yarn add @story-protocol/core-sdk viem
```
## Initiate SDK Client
Next we can initiate the SDK Client. There are two ways to do this:
1. Using a private key (preferable for some backend admin)
2. JSON-RPC account like Metamask where users sign their own transactions
### Set Up Private Key Account
Check out the TypeScript Tutorial for a working example of how to set up the
Story SDK Client.
Before continuing with the code below:
1. Make sure to have `WALLET_PRIVATE_KEY` set up in your `.env` file.
* Don’t forget to fund the wallet with some testnet tokens from a [Faucet](/network/network-info/aeneid#faucet)
2. Make sure to have `RPC_PROVIDER_URL` set up in your `.env` file.
* You can use the public default one (`https://aeneid.storyrpc.io`) or check out the other RPCs [here](/network/network-info/aeneid#rpcs).
```typescript utils.ts theme={null}
import { http } from "viem";
import { Account, privateKeyToAccount, Address } from "viem/accounts";
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
const privateKey: Address = `0x${process.env.WALLET_PRIVATE_KEY}`;
const account: Account = privateKeyToAccount(privateKey);
const config: StoryConfig = {
account: account, // the account object from above
transport: http(process.env.RPC_PROVIDER_URL),
chainId: "aeneid",
};
export const client = StoryClient.newClient(config);
```
### Setup for React (ex. Metamask)
The [React Setup Guide](/developers/react-guide/setup/overview) shows how we can also use the TypeScript SDK to delay signing & sending transactions to a JSON-RPC account like Metamask.
# Explain Like I'm Five
Source: https://docs.story.foundation/explain-like-im-five
An easy to understand overview of Story.
# FAQ
Source: https://docs.story.foundation/faq
Get answers to the most common questions about Story as a whole.
\$IP
Your preference obviously, since you can use any EVM-based wallet. But we
recommend [MetaMask](https://metamask.io/) for [OKX](https://www.okx.com/web3). You can add Story's L1 below:
Connect your wallet to Story's mainnet.
Connect your wallet to Story's 'Aeneid' testnet.
Check out our [Ecosystem - Getting Started](https://storyprotocol.notion.site/Story-Ecosystem-Getting-Started-169051299a5480cc9b3dcac7c3ec82da) page.
We don't have a native stablecoin right now. You can use bridge USDC.e powered by [Stargate](https://stargate.finance/bridge).
Use [Stargate](https://stargate.finance/bridge), [deBridge](https://app.debridge.finance/?inputChain=1\&outputChain=1514\&inputCurrency=\&outputCurrency=0xf1815bd50389c46847f0bda824ec8da914045d14\&dlnMode=simple\&address=\&amount=1), [Orbiter Finance](https://www.orbiter.finance/en?tgt_chain=1514\&src_chain=1\&src_token=ETH)
Story isn't replacing the legal system, it's providing on-chain rails to make the legal system more efficient for creative IP.
We have worked with world class legal teams to craft a real, off-chain legal contract called the [Programmable IP License (PIL💊)](/concepts/programmable-ip-license) that has simple terms allowing creators to state who can remix, monetize, and create derivatives of their IP and at what cost.
We've then built business logic on-chain (in the form of smart contracts) to automate & enforce those terms. This creates a tight mapping between the legal world and our on-chain terms.
You would mint an NFT that represents your off-chain asset, and register that
NFT on Story.
Automatic royalty flow can be enforced off-chain. Although the royalty infrastructure is on-chain, the associated license is valid beyond the chain. To legally abide by the license, any derivative work will need to pay royalties on-chain to the IP Account that is attached to your IP Asset, or face consequences like in the real world (e.g. being taken to court for abusing a license) according to the terms set out by the license.
Furthermore, one of the terms of the [Programmable IP License (PIL💊)](/concepts/programmable-ip-license) is that licensees are obligated to provide revenue data for off-chain transactions (e.g. merch) to licensors, if there's a revenue share involved.
Imagine you're an artist who creates digital paintings, or a musician who makes original songs. You want to share your work online, but you want to ensure that if others use or change your work, they give you credit and—if they make money from it—you get a share. That’s where Story comes in. It's a platform that uses technology to give IP owners like you control over how your work is used, tracked, and shared, so it’s both protected and fairly rewarded.
Think of it like this: Suppose you upload a song to Story. Now, anyone can see that you’re the original creator, and if someone wants to remix it, they can do so through Story. The system then automatically tracks the remix as a "derivative" of your song and notes you as the original artist. This way, if the remix becomes popular and earns money, Story can help you earn a portion of those earnings, just like the remixer.
The natural question arises: "*What if I'm a bad actor and I ignore all of this and just Right Click Save As?*"
First, its underestimated the extent to which people want to follow the law. This is why PIL is so important - all of the IP is not just on-chain, it is tied to a real legal contract! If people rip off your IP, they can be sued in court. However this "happy path" doesn't always happen. When things go wrong, we want to provide as many layers of escalation before resorting to off-chain arbitration.
Thus, we've created the [❌ Dispute Module](/concepts/dispute-module) that allows anyone to flag violating content on-chain. If the dispute is successful, that IP will be flagged and no longer able to monetize or generate licenses.
The worst case scenario on Story is the best case scenario anywhere else: legal arbitration. Creators using us can always use the traditional legal system as a backstop. This is a situation we want to avoid, but one that's necessary for creators to have trust in the system.
We started as a protocol, supporting projects on Polygon, Ethereum, and more. But this siloed IP to the chain that it lived on, and did not connect IP between chains. This does not realize the vision of Story as a global IP repo.
We want to be the IP settlement layer, or in other words, bring composability across chains. We need a single hub blockchain for all registered IP. This IP can be on Story, on other chains, or off-chain, but we need a hub such that all the licenses, royalties, and IP metadata live in a single unified execution environment.
First and most obvious, building our own Layer 1 allows us to innovate across the entire technical stack. This enables us to implement essential features like on-chain dispute resolution or on-chain IP graphs without waiting on existing L1 roadmaps.
The need to innovate the entire stack ourselves was proven when we tried to build on a few existing chains and quickly realized it would not work due to technical limitations of those chains. Here are a few reasons we decided - or rather were forced - to build an L1:
1. Existing L1s are simply not able to handle the registration of IP upon 1000s of parents - each with their own complicated licensing details - in one transaction efficiently. Gas usage on Geth EVM rapidly increases with ancestor IPs, making it impractical beyond 670 ancestors due to block gas limits. So we improved the efficiency of graph traversing and storing for our IP graph by leveraging a new stateful precompile approach, written directly in the execution client. This creates a "shortcut" around EVM to write and read certain values in the blockchain we use for our graphs, under the restrictions of PoC protocol.
2. Similarly, existing L1s are not able to handle the royalty token flow between 1000s of IP Assets. Again, this was solved by making improvements directly in the execution client.
3. Max composability by having license data on-chain, allows for 100s of IPs to remix at once.
4. Potential rollup support with bespoke X-CHAIN data (e.g. [Initia's](https://initia.xyz/) model) for Web2 apps with millions of transactions requiring rollups. Also support Web2 apps running their own validators for max incentive alignment.
5. Future validator-enshrined L1 tech like native oracles for NFTs and off-chain RWA IPs (Cosmos SDK vote extensions).
We actually did build an L2 before deciding to build an L1. However, we not only wanted to innovate the execution layer (also possible on L2), but add in a consensus mechanism, make improvements to validator node storage, novel validator features, and most importantly precompiles.
For example, we eventually want to allow Netflix, TikTok, etc to run validators to stream IP data directly on-chain, and also allow those nodes to have graph DBs optimized for IP graphs. Imagine any transaction involving a disputed IP Asset being immediately rejected.
The alternative is running an adjacent system outside the L1 to provide these features, which will strictly be less decentralized than the L1 itself. By making these features native to L1 (validators), we can have sufficient decentralization, guaranteed execution, and less system to maintain.
Namely, if the adjacent system goes down (so disputes and oracles stop) but the L1 works fine, it'd be a huge problem. This can be remedied with re-staking like AVS but the tech is not battle-tested and there's no precedence of success using re-staking (EIGEN token is still in work).
One big incentive alignment Story can have: IP companies running validators & providing custom off-chain IP data to the network natively via validator-enshrined features.
Or a law firm automating disputes and broadcasting them to other validators. After an agreement of disputes, validators can immediately block transactions that include disputed IPs, which is not possible with an adjacent system providing (unless we have preconf, which is a debated topic in the Ethereum land).
The IP graph must be on-chain because certain on-chain features require the ability to traverse and aggregate the IP graph. For example, royalty and revenue distribution need to occur on-chain through the IP graph. Using off-chain graph indexing would make these on-chain features either unfeasible or overly complex, as it would necessitate involving an oracle.
Similarly, the dispute module can check if an IP has been flagged due to its ancestor being flagged by traversing the IP graph directly on-chain, and then take immediate action such as aborting a transaction that would license a disputed IP without the need of an oracle.
We will support a few ways, including the [❌ Dispute Module](/concepts/dispute-module), to deter IP infringement. For example if someone were to register someone else's IP, it could be disputed on-chain. And in the worst case, it would be brought to court just as it works in the traditional legal system today.
A more nuanced answer to this (one that we're constantly exploring/improving upon) is there may be additional ways to deter IP infringement. For example, a staking validation mechanism where users could stake tokens on a piece of IP being valid, and if it were to be disputed and marked as copyright, the tokens get slashed and distributed to the creator who was harmed. Additionally we've thought of introducing external IP infringement detection services directly into our L1 at the lowest level that could flag or automatically mark IP as potential infringement the moment its registered.
Ultimately Story is not a system built to prevent bad actors, rather it is meant to help facilitate honest actors to more easily register their IP, remix from others, and set proper terms for their work. The protocol is permissionless and stopping bad actors entirely would be near impossible, but we can try to disincentivize them as best we can. Much like how the pirating of media plummeted when Apple Music, Spotify, and Netflix made such media more accessible by creating a "path of least resistance", we see a similar future with Story & IP.
View the [disclaimers](/foundation/disclaimer).
# Introduction
Source: https://docs.story.foundation/introduction
Welcome to Story
## Introducing the World's IP Blockchain
Story is a purpose-built layer 1 blockchain designed specifically for intellectual property.
You can register your IP on-chain and add usage terms to it in seconds, massively lowering the barrier to the currently complex & antiquated legal system for IP. For example, enforcing "you owe me 50% of your commercial revenue if you use my IP" without needing time, money, or lawyers.
IP could be an image, a song, an RWA, AI training data, or anything
in-between.
By making IP programmable on the blockchain, it becomes this transparent & decentralized global IP repository where AI agents (or any other software) and humans alike can transact on & monetize IP with a simple API call.
Read a 1 minute summary.
Read the Story whitepaper.
When IP owners share their work online, it’s easy for others to use or change it without crediting them, and they often don't get paid fairly if their work becomes popular or valuable. This can be discouraging for people who want to share their ideas and creations but don’t want to lose control over them.
Additionally, the sheer speed and superabundance of AI-generated media is outpacing the current IP system that was designed for physical replication. In many cases, [AI is trained on and is producing copyrighted data](https://twitter.com/BriannaWu/status/1823833723764084846).
Story fixes this by providing a way for creators to share their work with built-in protection. When someone (including an AI model) uses a song, image, or any creative work that’s registered on Story, the system automatically tracks who the original owner is and makes sure they get credited. Plus, if that work generates revenue—say someone remixes a song and it earns money—the original owner automatically receives their fair share based on license terms that were set on-chain.
## Quick FAQs
\$IP
Your preference obviously, since you can use any EVM-based wallet. But we
recommend [MetaMask](https://metamask.io/) for [OKX](https://www.okx.com/web3). You can add Story's L1 below:
Connect your wallet to Story's mainnet.
Connect your wallet to Story's 'Aeneid' testnet.
Check out our [Ecosystem - Getting Started](https://storyprotocol.notion.site/Story-Ecosystem-Getting-Started-169051299a5480cc9b3dcac7c3ec82da) page.
You can use bridged USDC.e powered by [Stargate](https://stargate.finance/bridge).
Use [Stargate](https://stargate.finance/bridge), [deBridge](https://app.debridge.finance/?inputChain=1\&outputChain=1514\&inputCurrency=\&outputCurrency=0xf1815bd50389c46847f0bda824ec8da914045d14\&dlnMode=simple\&address=\&amount=1), or [Orbiter Finance](https://www.orbiter.finance/en?tgt_chain=1514\&src_chain=1\&src_token=ETH).
## Brief Architecture Overview
There are several elements that make up Story as a whole. Below we will cover the most important components.
### Story Network: "The World's IP Blockchain"
Story Network is a purpose-built layer 1 blockchain achieving the best of EVM and Cosmos SDK. It is 100% EVM-compatible alongside deep execution layer optimizations to support graph data structures, purpose-built for handling complex data structures like IP quickly and cost-efficiently. It does this by:
* using precompiled primitives to traverse complex data structures like IP graphs within seconds at marginal costs
* a consensus layer based on the mature CometBFT stack to ensure fast finality and cheap transactions
### "Proof-of-Creativity" Protocol
Our "Proof-of-Creativity" Protocol, made up of smart contracts written in Solidity, are natively deployed on Story Network and allow anyone to onramp IP to Story. Most of our documentation focuses on the protocol.
Creators register their IP as [🧩 IP Assets](/concepts/ip-asset) on Story. You use [🧱 Modules](/concepts/modules) to interact with IP Assets. For example, enforcing proper usage of your IP via the [Licensing Module](/concepts/licensing-module), paying & claiming revenue via the [Royalty Module](/concepts/royalty-module), and disputing infringing IP via the [Dispute Module](/concepts/dispute-module).
Each IP Asset has an associated ERC721 NFT, which represents *ownership* over the IP. This means IP ownership can be bought & sold. Additionally, the licenses minted from an IP are also ERC721 NFTs, meaning you can buy & sell the rights to use an IP under certain terms. Together, this unlocks a new realm of **IPFi**.
### Programmable IP License
Although on-chain, an IP's usage terms and minted licenses are enforced by an off-chain legal contract called the [Programmable IP License (PIL💊)](/concepts/programmable-ip-license).
The PIL allows anyone to offramp tokenized IP on Story into the "real world" legal system and outlines real legal terms for how creators can remix, monetize, and create derivatives of their IP. *The protocol, or more specifically the IP Assets and modules described above, are what automate and enforce those terms on-chain*, creating a mapping between the legal world (PIL) and the blockchain.
Like USDC enables redemption for fiat, the PIL enables redemption for IP.
## Examples
Imagine you're an artist who creates digital paintings, or a musician who makes original songs. You want to share your work online, but you want to ensure that if others use or change your work, they give you credit and—if they make money from it—you get a share. That’s where Story comes in. It's a platform that uses technology to give IP owners like you control over how your work is used, tracked, and shared, so it’s both protected and fairly rewarded.
Think of it like this: Suppose you upload a song to Story. Now, anyone can see that you’re the original creator, and if someone wants to remix it, they can do so through Story. The system then automatically tracks the remix as a "derivative" of your song and notes you as the original artist. This way, if the remix becomes popular and earns money, Story can help you earn a portion of those earnings, just like the remixer.
Let’s say a scientist uploads an image dataset to be used by artificial intelligence (AI) models for research. Through Story, that dataset is registered, so if any company uses it to train their AI, the original scientist is credited. If that dataset then contributes to a profitable AI application, Story ensures a fair share goes to the original contributor.
With Story, you can share your work freely, knowing that wherever it goes, it’s tracked and fairly credited back to you. The idea is to create a fair environment for sharing, building upon, and growing creative work.
# Testnet - Aeneid
Source: https://docs.story.foundation/network/connect/aeneid
Information and resources for the Story Testnet (Aeneid)
Connect your wallet to Story's 'Aeneid' testnet.
# Resources
**Network Name**: Story Aeneid Testnet
**Chain ID**: 1315
**Chainlist Link**: [https://chainlist.org/chain/1315](https://chainlist.org/chain/1315)
## RPCs
| RPC Name | RPC URL | Official |
| :----------------- | :--------------------------------------------------------- | :------: |
| Story | `https://aeneid.storyrpc.io` | ✅ |
| Story by Alchemy | `https://story-aeneid.g.alchemy.com/v2/${ALCHEMY_API_KEY}` | |
| Story by QuickNode | `https://www.quicknode.com/chains/story` | |
| Story by Ankr | `https://rpc.ankr.com/story_aeneid_testnet` | |
## Explorers
| Explorer | URL | Official |
| :----------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :------: |
| [Blockscout Explorer ↗️](https://aeneid.storyscan.io) | `https://aeneid.storyscan.io` | ✅ |
| [IP Explorer ↗️](https://aeneid.explorer.story.foundation) (only for IP-related actions like licensing, minting licenses, etc) | `https://aeneid.explorer.story.foundation` | ✅ |
| [Stakeme Explorer ↗️](https://aeneid.storyscan.app/) | `https://aeneid.storyscan.app/` | |
## Faucet
| Faucet | Amount |
| :------------------------------------------------------------ | :----- |
| [Official Faucet ↗️](https://aeneid.faucet.story.foundation/) | 10 IP |
## Staking Dashboard
| Dashboard URL | Official |
| :---------------------------------------------------------- | :------: |
| [Story Dashboard](https://aeneid.staking.story.foundation/) | ✅ |
## Contract deployment addresses
* [Proof of Creativity](/developers/deployed-smart-contracts)
# Mainnet
Source: https://docs.story.foundation/network/connect/mainnet
Information and Resources for the Story's Mainnet
Connect your wallet to Story's Mainnet.
# Resources
**Network Name**: Story Mainnet
**Chain ID**: 1514
**Chainlist Link**: [https://chainlist.org/chain/1514](https://chainlist.org/chain/1514)
## RPCs
| RPC Name | RPC URL | Official |
| :----------------- | :---------------------------------------------------------- | :------: |
| Story | `https://mainnet.storyrpc.io` | ✅ |
| Story by Alchemy | `https://story-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}` | |
| Story by QuickNode | `https://www.quicknode.com/chains/story` | |
| Story by Ankr | `https://rpc.ankr.com/story_mainnet` | |
## Block Explorers
| Explorer | URL | Official |
| :---------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------- | :------: |
| [BlockScout Explorer ↗️](https://www.storyscan.io/) | `https://www.storyscan.io/` | ✅ |
| [IP Explorer ↗️](https://explorer.story.foundation) (only for IP-related actions like licensing, minting licenses, etc) | `https://explorer.story.foundation` | ✅ |
| [Stakeme Explorer ↗️](https://storyscan.app/) | `https://storyscan.app/` | |
| [Nodes.Guru ↗️](https://story.explorers.guru/) | `https://story.explorers.guru/` | |
| [CroutonDigital ↗️](https://explorer.crouton.digital/mainnets/story/overview) | `https://explorer.crouton.digital/mainnets/story/overview` | |
| [DeSpread ↗️](https://vp.despreadlabs.io/explorer/mainnet/story) | `https://vp.despreadlabs.io/explorer/mainnet/story` | |
| [Noders ↗️](https://ipstoryhub.org/explorer) | `https://ipstoryhub.org/explorer` | |
## Staking & Validator Dashboard
| Dashboard URL | Official |
| :----------------------------------------------------------------------------------------- | :------: |
| [Story Dashboard](https://staking.story.foundation/) | ✅ |
| [Node.Guru](https://story.explorers.guru/) | |
| [Krews](https://story-dashboard.krews.xyz/story/validators) | |
| [IT Rocket](https://itrocket.net/services/mainnet/story/analytics/validators-performance/) | |
## Contract deployment addresses
* [Proof of Creativity](/developers/deployed-smart-contracts)
# Consensus Layer (CL)
Source: https://docs.story.foundation/network/learn/node-software/consensus_layer
The **Consensus Layer (CL)** is built on the Cosmos SDK and CometBFT. The Cosmos SDK provides a modular framework for building blockchain applications, enabling seamless integration of new modules and features while allowing the network to be easily extended and customized. `story` client introduces upgrades and additional Cosmos SDK modules to support Engine API integration and novel staking mechanisms. CometBFT, a high-performance, scalable, and secure consensus engine, has been extensively tested within the Cosmos ecosystem. CometBFT and Cosmos SDK communicate through ABCI++ interface(link to ABCI++ spec). Checkout [story](https://github.com/piplabs/story) repo to review the codes.
# Overview
Source: https://docs.story.foundation/network/learn/node-software/cosmos-modules/cosmos-module-overview
List of all production-grade modules used on the Story blockchain
# List of Modules
Here is a list of all production-grade modules that can be used on the Story blockchain, along with their respective documentation:
* [evmengine](/network/node-architecture/cosmos-modules/evmengine-module) - Handles Cosmos-side logics on each EVM state transition via the [Engine API](/network/node-architecture/engine-api).
* [evmstaking](/network/node-architecture/cosmos-modules/evmstaking-module) - Handles staking and network emission logics with queues.
* [mint](/network/node-architecture/cosmos-modules/mint-module)
## Cosmos SDK (modified)
Story network uses the following Cosmos SDK modules with some modifications:
* [staking](/network/node-architecture/cosmos-modules/staking-module)
* [distribution](https://docs.cosmos.network/main/build/modules/distribution)
## Cosmos SDK (unmodified)
Story network uses the following Cosmos SDK modules without non-trivial modifications:
* [auth](https://docs.cosmos.network/main/build/modules/auth)
* [bank](https://docs.cosmos.network/main/build/modules/bank)
* [consensusparams](https://docs.cosmos.network/main/build/modules/consensus)
* [gov](https://docs.cosmos.network/main/build/modules/gov)
* [slashing](https://docs.cosmos.network/main/build/modules/slashing)
* [upgrade](https://docs.cosmos.network/main/build/modules/upgrade)
# EVM engine module
Source: https://docs.story.foundation/network/learn/node-software/cosmos-modules/evmengine-module
Module that facilitates communication between consensus and execution layers
## Abstract
This document specifies the internal `x/evmengine` module of the Story blockchain.
As Story Network separates the consensus and execution client, like Ethereum, the consensus client (CL) and execution client (EL) needs to communicate to sync to the network, propose proper EVM blocks, and execute EVM-triggered EL actions in CL.
The module exists to facilitate all communications between CL and EL using the [Engine API](/network/node-architecture/engine-api), from staking and upgrades to driving block production and consensus in CL and EL.
## Contents
1. **[State](#state)**
2. **[Prepare Proposal](#prepare-proposal)**
3. **[Process Proposal](#process-proposal)**
4. **[Post Finalize](#post-finalize)**
5. **[Messages](#messages)**
6. **[UBI](#ubi)**
7. **[Upgrades](#upgrades)**
## State
### Build Delay
Type: `time.Duration`
Build delay determines the wait duration from the start of `PrepareProposal` ABCI2 call before fetching the next EVM block data to propose from EL via the [Engine API](/network/node-architecture/engine-api). Applicable to the current proposer only. If the node has a block optimistically built beforehand, the build delay is not used.
### Build Optimistic
Type: `bool`
Enable optimistic building of a block if true. A node will deterministically build the next block if it finds itself as the next proposer in the current block. Optimistic building starts with requesting the next EVM block data (for the next CL block) immediately after the `FinalizeBlock` of ABCI2.
### Head Table
Type: `ExecutionHeadTable`
Head table stores the latest execution head data to be used for partial validation of EVM blocks received from other validators. When the chain initializes, the execution head is populated with the genesis execution hash loaded from `genesis.json`.
The following execution head is stored in the table.
```protobuf protobuf theme={null}
message ExecutionHead {
option (cosmos.orm.v1.table) = {
id: 1;
primary_key: { fields: "id", auto_increment: true }
};
uint64 id = 1; // Auto-incremented ID (always and only 1).
uint64 created_height = 2; // Consensus chain height this execution block was created in.
uint64 block_height = 3; // Execution block height.
bytes block_hash = 4; // Execution block hash.
uint64 block_time = 5; // Execution block time.
}
```
### Upgrade Contract
Type: `*bindings.UpgradeEntrypoint`
Upgrade contract is used to filter and parse upgrade-related events from EL.
### UBI Contract
Type: `*bindings.UBIPool`
UBI contract is used to filter and parse UBI-related events from EL.
### Mutable Payload
Type: struct
Mutable payload stores the optimistic block built, if optimistic building is enabled.
#### Genesis State
The module's `GenesisState` defines the state necessary for initializing the chain from a previously exported height.
```protobuf protobuf theme={null}
message GenesisState {
Params params = 1 [(gogoproto.nullable) = false];
}
message Params {
bytes execution_block_hash = 1 [
(gogoproto.moretags) = "yaml:\"execution_block_hash\""
];
}
```
## Prepare Proposal
At each block, if the node is the proposer, ABCI2 triggers `PrepareProposal` which
1. Loads staking & reward withdrawals from the [evmstaking](/network/node-architecture/cosmos-modules/evmstaking-module) module.
2. Builds a valid EVM block.
* If optimistic building: loads the optimistically built block.
* Non-optimistic: requests and retrieves an EVM block from EL.
3. Collects the EVM logs of the previous/parent block.
4. Assembles `MsgExecutionPayload` with the built EVM block and previous EVM logs.
5. Returns a transaction containing the assembled `MsgExecutionPayload` data.
This CL block is then propagated to all other validators.
## Process Proposal
At each block, if the node is not a proposer but a validator, ABCI2 triggers `ProcessProposal` with received commits (which should be a transaction of `MsgExecutionPayload` data in the honest case).
The node first validates that the received commit has only one transaction with at least 2/3 of votes committed. Then, the node validates that the one transaction only contains one unmarshalled `MsgExecutionPayload` data. Finally, the node processes the received data and broadcasts its acceptance of the proposal to the network. If any of the validation or processing fails, the node rejects the proposal.
More specifically, the node processes the received `MsgExecutionPayload` data in the following manner:
1. Validates the fields of the received `MsgExecutionPayload` (outlined in [Messages](#msgexecutionpayload)).
2. Compare local stake & reward withdrawals with the received withdrawals data.
3. Push the received execution payload to EL via the Engine API and wait for payload validation.
4. Update the EL forkchoice to the execution payload's block hash.
5. Process staking events using the [evmstaking](/network/node-architecture/cosmos-modules/evmstaking-module) module.
6. Process upgrade events.
7. Update the execution head to the execution payload (finalized block).
## Post Finalize
If optimistic building is enabled, `PostFinalize` is triggered immediately after `FinalizeBlock` set through custom ABCI callback. During this process, the node peeks the staking and reward queues from the evmstaking module, and builds a new execution payload on top of the current execution head. It sets the optimistic block to be used in the next block's `PrepareProposal` phase and returns the response from the forkchoice update.
## Messages
In this section we describe the processing of the evmengine messages and the corresponding updates to the state. All created/modified state objects specified by each message are defined within the state section.
### MsgExecutionPayload
```protobuf protobuf theme={null}
message MsgExecutionPayload {
option (cosmos.msg.v1.signer) = "authority";
string authority = 1;
bytes execution_payload = 2;
repeated EVMEvent prev_payload_events = 3;
}
message EVMEvent {
bytes address = 1;
repeated bytes topics = 2;
bytes data = 3;
bytes tx_hash = 4;
}
```
This message is expected to fail if:
* authority is invalid (not evmengine authority)
* execution payload fails to unmarshal to [ExecutableData](https://github.com/piplabs/story/blob/c38b80c13579d3df7174ea10c3368ef0692f52da/client/x/evmengine/types/executable_data.go#L17-L35) for reasons such as invalid fields
* execution payload's block number does not match CL head's block number + 1
* execution payload's block parent hash does not match CL head's hash
* execution payload's timestamp is invalid
* execution payload's RANDAO does not match CL head's hash (ie. parent hash)
* execution payload's `Withdrawals`, `BlobGasUsed`, and `ExcessBlobGas` fields are nil
* execution payload's `Withdrawals` count does not match local node's sum of dequeued stake & reward withdrawals
The message must contain previous block's events, which gets processed at the current CL block (in other words, execution events from EL block n-1 are processed at CL block n). In the future, the message will remove `prev_payload_events` and rely on [Engine API](/network/node-architecture/engine-api) to get the current finalized EL block's events.
Also note that EVM events are processed in CL in the order they are generated in EL.
## UBI
All UBI-related changes must be triggered from the canonical UBI contract in the EVM execution layer. This module handles the execution handling of those triggers in CL. Read more about [UBI for validators](/network/tokenomics-staking#ubi-for-validators)
### Set UBI Distribution
The `UBIPool` contract emits the UBI distribution set event, which is parsed by the module to set the UBI percentage in the distribution module.
## Upgrades
All chain upgrade-related logics must be triggered from the canonical upgrade contract in the EVM execution layer. This module handles the execution handling of those triggers in CL.
### Software Upgrade
The `UpgradeEntrypoint` contract emits the software upgrade event, which is parsed by the module to schedule an upgrade at a given height for a given binary name. Currently, all upgrades must either be set via forks or by the software upgrade events; the latter process is a multisig-controlled process, which will transition into a voting-based process in the future.
### Cancel Upgrade
Similar to the software upgrade, the module processes the cancel upgrade event from EVM logs of the previous block, and clears an existing upgrade plan.
# EVM staking module
Source: https://docs.story.foundation/network/learn/node-software/cosmos-modules/evmstaking-module
Module that facilitates consensus layer staking-related logic
## Abstract
This document specifies the internal `x/evmstaking` module of the Story blockchain.
In Story blockchain, the gas token resides on the execution layer (EL) to pay for transactions and interact with smart contracts. However, the consensus layer (CL) manages the consensus staking, slashing, and rewarding. This module exists to facilitate CL-level staking-related logic, such as delegating to validators with custom lock periods.
## Contents
1. **[State](#state)**
2. **[Two Queue System](#two-queue-system)**
3. **[Withdrawal Queue Content](#withdrawal-queue-content)**
4. **[End Block](#end-block)**
5. **[Processing Staking Events](#processing-staking-events)**
6. **[Withdrawing Delegations](#withdrawing-delegations)**
7. **[Withdrawing Rewards](#withdrawing-rewards)**
8. **[Withdrawing UBI](#withdrawing-ubi)**
## State
### Withdrawal Queue
Type: `Queue[types.Withdrawal]`
The (stake) withdrawal queue stores the pending unbonded stakes to be burned on CL and minted on EL. Stakes that are unbonded after 14 days of unstaking period are added to the queue to be processed.
### Reward Withdrawal Queue
Type: `Queue[types.Withdrawal]`
The reward withdrawal queue stores the pending rewards from stakes to be burned on CL and minted on EL. All rewards above a threshold are eligible to be queued in this queue, but there exists a parameter of maximum additions per block.
### Parameters
```protobuf protobuf theme={null}
message Params {
uint32 max_withdrawal_per_block = 1 [
(gogoproto.moretags) = "yaml:\"max_withdrawal_per_block\""
];
uint32 max_sweep_per_block = 2 [
(gogoproto.moretags) = "yaml:\"max_sweep_per_block\""
];
uint64 min_partial_withdrawal_amount = 3 [
(gogoproto.moretags) = "yaml:\"min_partial_withdrawal_amount\""
];
string ubi_withdraw_address = 4 [
(gogoproto.moretags) = "yaml:\"ubi_withdraw_address\""
];
}
```
* `max_withdrawal_per_block` is the maximum number of withdrawals (reward and unstakes, each) to process per block. This parameter prevents nodes from processing a large amount of withdrawals at once, which could exceed the max chain timeout.
* `max_sweep_per_block` is the maximum number of validator-delegator delegations to sweep per block. This parameter prevents nodes from processing a large amount of delegations at once.
* `min_partial_withdrawal_amount` is the minimum amount required for rewards to get added to the reward withdrawal queue.
* `ubi_withdrawal_address` is the UBI contract address to which UBI withdrawals should be deposited.
### Delegator Withdraw Address
Type: `Map[string, string]`
The delegator-withdraw address mapping tracks the address to which a delegator receives their withdrawn stakes. The (stake) withdrawal queue uses this map to determine the `execution_address` in the `Withdrawal` struct used in building an EVM block payload.
While the delegator can change the withdraw address at any time, existing stake withdraw requests in the (stake) withdrawal queue will maintain their original values.
### Delegator Reward Address
The delegator-reward address mapping tracks the address to which a delegator receives their reward stakes, similar to the delegator-withdraw mapping.
While the delegator can change the reward address at any time, existing reward withdraw requests in the reward withdrawal queue will maintain their original values.
Type: `Map[string, string]`
### Delegator Operator Address
Type: `Map[string, string]`
The delegator-operator address mapping tracks the address to which a delegator has given the privilege to delegate (stake), undelegate (unstake), and redelegate on behalf of themselves.
### IP Token Staking Contract
Type: `*bindings.IPTokenStaking`
IPTokenStaking contract is used to filter and parse staking-related events from EL.
## Two Queue System
The module departs from traditional Cosmos SDK staking module's unstaking system, where all unbonded entries (stakes that have unbonded after 14 days of unbonding period) are immediately distributed into delegators account. Instead, Story's unstaking system assimilates Ethereum 2.0's unstaking system, where 16 full or partial (reward) withdrawals are processed per slot.
In a single queue of withdrawals, reward withdrawals can significantly delay stake withdrawals. Hence, Story blockchain implements a two-queue system where a max amount to process per block is enforced per queue. In other words, the stake/ubi withdrawal and reward withdrawal queues can each process the max parameter per block.
## Withdrawal Queue Content
Since the module only processes unstakes/rewards/ubi and stores them in queues, the actual dequeueing for withdrawal to the execution layer is carried out in the [evmengine](/network/node-architecture/cosmos-modules/evmengine-module) module. More specifically, a proposer dequeues the max number of withdrawals from each queue and adds them to the EVM block payload, which gets executed by EL via the [Engine API](/network/node-architecture/engine-api). When validators receive proposed block payload from the proposer, they individually peek the local queues and compare them against the received block's withdrawals. Mismatching withdrawals indicate non-determinism in staking logics and should result in chain halt.
In other words, the `evmstaking` module is in charge of parsing, processing, and inserting withdrawal requests to two queues, while the `evmengine` module is in charge of validating and dequeuing withdrawal requests, as well as depositing them to corresponding withdrawal addresses in EL.
## End Block
The `EndBlock` ABCI2 call is responsible for fetching the unbonded entries (stakes that have unbonded after 14 days) from the [staking](/network/node-architecture/cosmos-modules/staking-module) module and inserting them into the (stake) withdrawal queue. Furthermore, it processes stake reward withdrawals into the reward withdrawal queue and UBI withdrawals into the (stake) withdrawal queue.
If the network is in the [Singularity period](/network/tokenomics-staking#singularity), the End Block is skipped as there are no staking rewards and withdrawals available during this period. Otherwise, refer to [Withdrawing Delegations](#withdrawing-delegations) and [Withdrawing Rewards](#withdrawing-rewards) for detailed withdrawal processes.
## Processing Staking Events
The module parses and processes staking events emitted from the [IPTokenStaking contract](https://github.com/piplabs/story/blob/main/contracts/src/protocol/IPTokenStaking.sol), which are collected by the [evmengine](/network/node-architecture/cosmos-modules/evmengine-module) module. The list of events are:
### Staking events
* Create Validator
* Deposit (delegate)
* Withdraw (undelegate)
* Redelegate
* Unjail: anyone can request to unjail a jailed validator by paying the unjail fee in the contract.
These operations incur a fixed gas cost to prevent spam.
### Parameter events
* Update Validator Commission: update the validator commission.
* Set Withdrawal Address: delegator can modify their withdrawal address for future unstakes/undelegations.
* Set Reward Address: delegator can modify their withdrawal address for future reward emissions.
* Set Operator: delegator can modify their operator with privileges of delegation, undelegation, and redelegation.
* Unset Operator: delegator can remove operator.
These operations incur a fixed gas cost to prevent spam.
## Withdrawal
Both withdrawal queues hold withdrawals of type:
```protobuf protobuf theme={null}
message Withdrawal {
option (gogoproto.equal) = true;
option (gogoproto.goproto_getters) = false;
uint64 creation_height = 1;
string execution_address = 2 [
(cosmos_proto.scalar) = "cosmos.AddressString",
(gogoproto.moretags) = "yaml:\"execution_address\""
];
uint64 amount = 3 [
(gogoproto.moretags) = "yaml:\"amount\""
];
WithdrawalType withdrawal_type = 4 [
(gogoproto.moretags) = "yaml:\"withdrawal_type\""
];
string validator_address = 5 [
(gogoproto.moretags) = "yaml:\"validator_address\""
];
}
```
* `creation_height` is the block height at which the withdrawal is created.
* `execution_address` is the EVM address receiving the withdrawn fund, which is burned in CL.
* `amount` is the amount to burn on CL and mint on EL.
* `withdrawal_type` is the type of withdrawal: $0$ for unstakes, $1$ for reward, and $2$ for UBI.
* `validator_address` is the EVM validator address.
### Withdrawing Delegations
Delegations that have unbonded after 14 days of unbonding period (ie. unbonded entries) gets added to the (stake) withdrawal queue at the end of each block. If validator is totally-unstaked, ie. all delegations and self-delegations are unbonded, then validator's commission is also withdrawn.
### Withdrawing Rewards
Inflation rewards allocated to delegations are auto-swept at the end of each block. If a delegation's accrued reward is greater than the parameterized threshold, the reward is added to the reward withdrawal queue to be credited to the delegator's EVM reward address.
# Token Minting Module
Source: https://docs.story.foundation/network/learn/node-software/cosmos-modules/mint-module
Module responsible for token minting and inflation in the Story blockchain
## Contents
1. [Contents](#contents)
2. [State](#state)
3. [Begin Block](#begin-block)
4. [Parameters](#parameters)
5. [Events](#events)
## State
### Params
* Params: `mint/params -> legacy_amino(params)`
```protobuf protobuf theme={null}
message Params {
option (amino.name) = "client/x/mint/Params";
// type of coin to mint
string mint_denom = 1;
// inflation amount per year
string inflations_per_year = 2 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
// expected blocks per year
uint64 blocks_per_year = 3;
}
```
## Begin Block
Minting parameters are calculated and inflation paid at the beginning of each block.
### Inflation amount calculation
Inflation amount is calculated using an "inflation calculation function" that's\
passed to the `NewAppModule` function. If no function is passed, then the SDK's
default inflation function will be used (`DefaultInflationCalculationFn`). In case a custom
inflation calculation logic is needed, this can be achieved by defining and
passing a function that matches `InflationCalculationFn`'s signature.
```go theme={null}
type InflationCalculationFn func(ctx sdk.Context, minter Minter, params Params, bondedRatio math.LegacyDec) math.LegacyDec
```
## Parameters
The minting module contains the following parameters:
| Key | Type | Example |
| ----------------- | --------------- | ------------------- |
| MintDenom | string | "stake" |
| InflationsPerYear | string (dec) | "20000000000000000" |
| BlocksPerYear | string (uint64) | "10368000" |
* `MintDenom` is the coin denominator used.
* `InflationsPerYear` is the target inflation per year, in 1e18 decimals.
* `BlocksPerYear` is the target number of blocks per year.
## Events
The minting module emits the following events:
### BeginBlocker
| Type | Attribute Key | Attribute Value |
| :--- | :------------ | :-------------- |
| mint | amount | "1000" |
# Staking Module
Source: https://docs.story.foundation/network/learn/node-software/cosmos-modules/staking-module
Modified staking module with reward multipliers for locked and unlocked tokens
## Abstract
The staking module has been modified to accommodate for the following changes below. Refer to the Cosmos SDK's [staking module docs](https://docs.cosmos.network/main/build/modules/staking) for more information.
## Reward Multiplier
### Validators
Validators can choose to accept either locked tokens or unlocked tokens as delegations. Validators for locked tokens are conditioned to half the inflation allocation of validators for unlocked tokens.
Since each validator receives different inflation distribution based on delegations, the inflation distribution Ivi for validator vi in the rewards pool is calculated as follows:
where
* Ivi is the total inflationary token rewards for vi
* Svi is the staked tokens for vi
* Mvi is the rewards multiplier for vi
* 0.5 for locked tokens
* 1 for unlocked tokens
* Rn is the total inflationary tokens allocated for the rewards pool in block n, calculated in the [mint](/network/node-architecture/cosmos-modules/mint-module) module
### Delegations
Delegators can delegate with four different staking lock times, which results in different staking reward multiplier for each delegation (delegator-validator pair of stakes). The inflation distribution for each delegation Di is calculated as follows:
where
* Sdi is the staked tokens of delegation di on validator vd
* Mdi is the rewards multiplier of di on vd
* Iv is the total inflationary token rewards for vd
* Cv is the commission rate for vd
#### Time-weighted Reward Multiplier Mdi
* *Flexible* (no lockup): 1
* *Short* (90 days): 1.1
* *Medium* (360 days): 1.5
* *Long* (540 days): 2.0
# Engine API
Source: https://docs.story.foundation/network/learn/node-software/engine_api
The Engine API is a collection of JSON-RPC methods that facilitate communication between the execution layer (EL) and the consensus layer (CL) of an EVM node. Story's execution layer, which offers full EVM compatibility, supports all standard JSON-RPC methods defined by the [Ethereum Engine API](https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md). Meanwhile, Story's consensus layer, built on Cosmos modules, utilizes the Engine API to coordinate with the execution layer.
## Functionalities
The Engine API facilitates seamless interaction between the EL and the CL by providing essential coordination mechanisms, including:
* **Handshake**
* **Synchronization**
* **Block Validation**
* **Block Proposal**
## Execution Layer Implementation
The EL in Story implements the following standard Engine API methods to support these functionalities:
* `engine_exchangeCapabilities`: Exchanges supported methods.
* `engine_getClientVersion`: Exchanges client version data.
* `engine_newPayload`: Inserts the given payload into the local chain.
* `engine_forkchoiceUpdate`: Updates the canonical chain marker and generates the payload with given attributes.
* `engine_getPayload`: Retrieves the pre-generated payload.
## Consensus Layer Interaction
How does Story's Consensus Layer (CL) interact with these methods? The answer lies in CometBFT ABCI++.
CometBFT is a state machine replication engine which provides consensus and security for Cosmos modules. ABCI++, also known as ABCI 2.0, is the interface between CometBFT and the actual state machine being replicated(i.e. EL's state machine).
ABCI++ comprises of a set of methods that interact with the Engine API, as outlined below:
### **1. PrepareProposal** (Proposing a New Block)
* The CL checks whether a payload is already being generated using `payloadID`.
* If not, the CL calls `engine_forkchoiceUpdate` to trigger a new payload generation.
* The CL then calls `engine_getPayload` with `payloadID` to fetch the payload and propose a new block.
### **2. ProcessProposal** (Processing a New Block)
* The CL calls `engine_newPayload` to delivers the new block to the EL.
* The EL validates payload of the new block, executes transactions deterministically and updates its state.
### **3. FinalizeBlock** (Finalizing a Decided Block)
* The CL calls `engine_newPayload` to delivers the finalized block to the EL.
* If the block has not yet been incorporated into the EL, the EL validates payload of the new block, executes transactions deterministically and updates its state.
* Since CometBFT provides instant finality, the CL calls `engine_forkchoiceUpdate` to finalize the block.
* Finally, the CL calls `engine_forkchoiceUpdate` again, with extra attributes, to start an optimistic build of the next block if enabled, and if the validator is the next proposer.
This interaction ensures smooth coordination between the EL and the CL, maintaining the integrity and efficiency of Story's blockchain network.
# Execution Layer (EL)
Source: https://docs.story.foundation/network/learn/node-software/execution_layer
The **Execution Layer (EL)** is a fork of the Geth client, with the addition of the [IPGraph Precompile](/network/node-architecture/precompile) and [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) precompile. It handles transaction execution, broadcasting and state storage while maintaining full compatibility with the Ethereum Virtual Machine (EVM) and supporting all Ethereum JSON-RPC methods. Currently we support all features introduced by the Pectra upgrade. Checkout [story-geth](https://github.com/piplabs/story-geth) repo to review the codes.
# Run a localnet
Source: https://docs.story.foundation/network/learn/node-software/localnet
Guide to setting up and running a local Story network for development and testing
# Overview
You can easily set up your own local Story network using docker compose, consisting of one boot node and four validator nodes. With this local network, you can test the consensus layer of the Story network or deploy your application using the precompiled primitive, the IP graph, to conduct various tests. Additionally, you can reset the network at any time as needed.
# Run a Local Story Network
For more detailed information for running Story local network, please refer the repository: [https://github.com/piplabs/story-localnet](https://github.com/piplabs/story-localnet)
## Prerequisite
To set up a local network, [Docker](https://docs.docker.com/get-started/get-docker/) is required.
## Step 1 - Start Docker
Please run Docker.
## Step 2 - Clone Repository
You need to clone three repositories: `story`, `story-geth`, and `story-localnet`.\
Make sure all three repositories are located within the same subfolder.
```bash theme={null}
# clone repositories
git clone https://github.com/piplabs/story.git
git clone https://github.com/piplabs/story-geth.git
git clone https://github.com/piplabs/story-localnet.git
```
## Step 3 - Start Nodes
Navigate to story-localnet project and start the local network.
```bash theme={null}
# move to story-localnet
cd story-localnet
# start story local network
./start.sh
```
## Step 4 - Terminate Nodes
If you want to stop the Story local network, you can do so by executing the script below.
```bash theme={null}
# terminate story local network
./terminate.sh
```
***
## How to Allocate Token to Your Account from Genesis
You may need to allocate IP tokens to your account for testing in the local network.\
To allocate tokens to your account in the genesis block, follow these steps:
1. Add your account information to the alloc section in `config/story/genesis-geth.json`:
```json theme={null}
"": {
"nonce": "0x0",
"balance": "",
"code": "0x",
"storage": {}
}
```
2. Run the `update-genesis-hash.sh` script to update the genesis block hash:
```bash theme={null}
./update-genesis-hash.sh
```
***
## How to interact with Story Local Network
By default, the Story local network has the following ports open for interaction.
| Port | Service | Role |
| :---- | :--------- | :--------------------------------------------------------------------- |
| 8545 | story-geth | Endpoint of RPC server for Story execution client |
| 1317 | story-node | Endpoint of API server for interacting with the Story consensus client |
| 26657 | story-node | Endpoint of cosmos-sdk RPC server for Story consensus client |
***
## Monitoring Systems
This setup includes a monitoring stack to provide centralized metrics and logs\
visualization for the blockchain network. Tools include **Prometheus**,
**Loki**, **Promtail**, and **Grafana**, all integrated through Docker Compose.
### **Components and Access Information**
| Service | Role | Default Port | Access URL |
| :--------- | :---------------------------------------------------------------- | :----------------------------- | :---------------------- |
| Prometheus | Collects metrics from nodes and itself for performance monitoring | `9090` | `http://localhost:9090` |
| Loki | Aggregates and stores logs from the network nodes via Promtail | `3100` | `http://localhost:3100` |
| Promtail | Scrapes logs from Docker containers and sends them to Loki | `9080` (API), `9095` (Metrics) | `http://localhost:9080` |
| Grafana | Provides a dashboard interface for metrics and logs visualization | `3000` | `http://localhost:3000` |
# Learn about Story's node software
Source: https://docs.story.foundation/network/learn/node-software/overview
Story's node client software has been implemented to support full EVM equivalency and fast block time and one-shot finality.
Story's node software consists of two components: an [Execution Layer (EL)](/network/learn/node-software/execution_layer) responsible for the execution environment and a [ Consensus Layer (CL)](/network/learn/node-software/consensus_layer) responsible for the consensus and block formation.
These two layers communicate via the [Engine API](/network/learn/node-software/engine-api) presented [Here](https://hackmd.io/@danielrachi/engine_api).
# Precompiles
Source: https://docs.story.foundation/network/learn/node-software/precompiled-contracts
Specialized smart contracts implemented in Story Protocol's execution layer
## Introduction
Precompiled contracts are specialized smart contracts implemented directly in the execution layer of a blockchain. Unlike user-deployed smart contracts that execute EVM bytecode, precompiled contracts offer optimized native implementations for complex cryptographic and computational operations. This significantly improves efficiency and reduces gas costs. Precompiled contracts exist at fixed addresses within the execution client and each precompile has a predefined gas cost based on its computational complexity, ensuring predictable execution fees.
Story Protocol introduces two precompiled contracts:
* `p256Verify` precompile to support signature verifications in the secp256r1 elliptic curve.
* `ipgraph` precompile to enhance on-chain intellectual property management.
In addition, Story Protocol’s execution layer supports all standard EVM precompiled contracts, ensuring full compatibility with Ethereum-based tooling and applications.
## Precompiled Contracts
| Address | Functionality |
| ------- | ------------------------------------------------------------- |
| byte1 | `ecrecover`- ECDSA signature recovery |
| byte2 | `sha256` - SHA-256 hash computation |
| byte3 | `ripemd160` - RIPEMD-160 hash computation |
| byte4 | `identity` - Identity function |
| byte5 | `modexp` - Modular exponentiation |
| byte6 | `bn256Add` - BN256 elliptic curve addition |
| byte7 | `bn256ScalarMul` - BN256 elliptic curve scalar multiplication |
| byte8 | `bn256Pairing` - BN256 elliptic curve pairing check |
| byte9 | `blake2f` - Blake2 hash function |
| byte10 | `kzgPointEvaluation` - KZG polynomial commitment evaluation |
| byte0 | `p256Verify` - Secp256r1 signature verification |
| byte1 | `ipgraph` - Intellectual property management |
## p256Verify precompile
Refer to [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) for more information.
## IPgraph precompile
The `ipgraph` precompile enables efficient querying and modification of IP relationships and royalty structures while minimizing gas costs.
This contract is deployed at `0x0000000000000000000000000000000000000101` and access is controlled through this contract `0x1640A22a8A086747cD377b73954545e2Dfcc9Cad`.
This precompile provides multiple functions based on the function selector—the first 4 bytes of the input.
| Function Selector | Description | Gas computation formula | Gas Cost |
| :----------------------- | :-------------------------------------------------------------- | :---------------------------------------------------- | :--------------------------------- |
| `addParentIp` | Adds a parent IP record | `intrinsicGas + (ipGraphWriteGas * parentCount)` | Larger than 1100 |
| `hasParentIp` | Checks if an IP is parent of another IP | `ipGraphReadGas * averageParentIpCount` | 40 |
| `getParentIps` | Retrieves parent IPs | `ipGraphReadGas * averageParentIpCount` | 40 |
| `getParentIpsCount` | Gets the number of parent IPs | `ipGraphReadGas` | 10 |
| `getAncestorIps` | Retrieves ancestor IPs | `ipGraphReadGas * averageAncestorIpCount * 2` | 600 |
| `getAncestorIpsCount` | Gets the number of ancestor IPs | `ipGraphReadGas * averageParentIpCount * 2` | 80 |
| `hasAncestorIp` | Checks if an IP is ancestor of another IP | `ipGraphReadGas * averageAncestorIpCount * 2` | 600 |
| `setRoyalty` | Sets royalty details of an IP | `ipGraphWriteGas` | 1000 |
| `getRoyalty` | Retrieves royalty details of an IP | `varies by royalty policy` | LAP:900, LRP:620, other:1000 |
| `getRoyaltyStack` | Retrieves royalty stack of an IP | `varies by royalty policy` | LAP:50, LRP: 600, other:1000 |
| `hasParentIpExt` | Checks if an IP is parent of another IP through external call | `ipGraphExternalReadGas * averageParentIpCount` | 8400 |
| `getParentIpsExt` | Retrieves parent IPs through external call | `ipGraphExternalReadGas * averageParentIpCount` | 8400 |
| `getParentIpsCountExt` | Gets the number of parent IPs through external call | `ipGraphExternalReadGas` | 2100 |
| `getAncestorIpsExt` | Retrieve ancestor IPs through external call | `ipGraphExternalReadGas * averageAncestorIpCount * 2` | 126000 |
| `getAncestorIpsCountExt` | Gets the number of ancestor IPs through external call | `ipGraphExternalReadGas * averageParentIpCount * 2` | 16800 |
| `hasAncestorIpExt` | Checks if an IP is ancestor of another IP through external call | `ipGraphExternalReadGas * averageAncestorIpCount * 2` | 126000 |
| `getRoyaltyExt` | Retrieves royalty details of an IP through external call | `varies by royalty policy` | LAP:189000, LRP:130200, other:1000 |
| `getRoyaltyStackExt` | Retrieves royalty stack of an IP through external call | `varies by royalty policy` | LAP:10500, LRP:126000, other:1000 |
Refer to the [Royalty Module](/concepts/royalty-module) for detailed information on royalty policies.
# Learn about Story
Source: https://docs.story.foundation/network/learn/overview
**Story** is a purpose-built decentralized blockchain supercharged by a *multi-core* execution environment.
Its architecture comprises a main execution core alongside multiple highly customized cores. The main core provides EVM equivalence, enabling rapid adoption of existing applications from the ecosystem.
The Intellectual Property (IP) core, one of the specialized cores, efficiently handles intellectual property registration as a native asset class while optimizing operations across complex IP relationship graphs. This core transforms intelligence into programmable IP assets. Although Story focuses primarily on intellectual property, its flexible architecture enables the adoption of future cores that can expand far beyond IP-related applications.
Learn more about the architecture in the [whitepaper](https://www.story.foundation/whitepaper.pdf) and understand the token economy design in Story's [Staking Desgin](/network/learn/token-economy) documentation. For details on implementation, see the [Node Software](/network/learn/node-software) chapter.
# Staking Design
Source: https://docs.story.foundation/network/learn/token-economy
Detailed overview of Story Network's staking mechanics and tokenomics
# Purpose
This document walks through the staking specification for Story. The goal is to provide clarity to network participants and technical partners on how Story's staking mechanics work and how users can interface with our chain.
# Tokenomics
## Genesis
The story genesis allocation will consist of 1 billion tokens, distributed among ecosystem participants, the foundation, investors, and the core team. Please refer this document for the detailed [Token Distribution](https://www.story.foundation/blog/introducing-ip).
## Locked vs Unlocked tokens
Unlocked tokens have no restrictions imposed on them and can be used for gas consumption, transfers, and staking.
Unlike unlocked tokens, locked tokens cannot be transferred or traded and are unlocked based on an unlock schedule. However, locked tokens may be staked to earn staking rewards, with the locked staking reward rate being half of that of unlocked tokens.
Staked locked and unlocked tokens have the same voting power. That means that a validator with 100 staked locked tokens has the same network voting power as a validator with 100 staked unlocked tokens.
Both types of tokens can be slashed if their validators get slashed.
## Token emissions
A fixed number of tokens will be allocated for emissions in the first year, with the quantity determined by the foundation at Genesis. For subsequent years, the number of emitted tokens will be controlled by an emissions algorithm whose parameters may be updated via governance or subject to change via hard forks. The emissions per block are controlled by the following two parameters:
* blocks\_per\_year: 10368000 blocks
* The number of blocks expected to be produced in a year
* inflations\_per\_year: 20,000,000 tokens
* The total number of inflationary tokens to be emitted in a year
New emissions will flow to two places:
1. Block Rewards
2. Community Pool
## Token burn
Since Story uses a fork of geth as the execution client, the burning mechanism follows Ethereum's EIP-1559.
# Staking
> 🔗 [Stake with the Staking Dashboard ↗️](https://staking.story.foundation/)
Story supports the below staking-related operations
* Create validator
* Update validator commission
* Stake
* Stake on behalf
* Unstake
* Unstake on behalf
* Redelegate
* Redelegate on behalf
* Set withdraw address
* Set reward address
* Unjail
* Unjail on behalf
Before explaining the behavior of each of these operations, some high-level concepts like **Token Staking Types**, **Validator Set Status**, **Unbonding**, and **Staking Period** will be explained first:
## Token Staking Types
As staking is enabled for both locked and unlocked tokens, validators must choose which type of token staking they want to support. Once a token staking type is selected, validators cannot switch to a different type.
## Validator Set Status
In Story, validators are grouped into one of two sets, (1) the active (bonded) validator set, which participates in consensus and receives block rewards, or (2) the non-active (unbonded) validator set, which does not contribute to the consensus process. To be selected as part of the active validator set, a validator must be one of the top 64 validators ranked by staked tokens.
Note that all priority fees on Story go directly to the block proposer.
## Unbonding
Unstaking for delegators is subject to an unbonding process. Users must wait for an unbonding time before any tokens return to their accounts.
This is the same for validators who self-delegate to themselves. They also need to go through the unbonding process when they want to unstake.
The unbonding time is 14 days. During the unbonding period, the delegator/validator will not earn block rewards. But they may still be slashed.
For each validator/delegator pair, the maximum ongoing unbonding transactions is 14. More unbonding requests beyond this limit will fail.
## Staking period
Delegators can decide how flexible and how long they want to stake their tokens. By default, for both locked and unlocked tokens, delegators can stake and then unstake immediately and get their token back after the unbonding time. We call this **flexible staking** in this document.
For unlocked tokens, a few more fixed staking periods are supported: 90 days, 360 days, and 540 days. In this case, users can only call unstake after the staking period is mature. Any call earlier than the mature day will be discarded. Unstaking from a mature staking period is still subject to the unbonding process, meaning users will get their staked tokens back after 14 days of unbonding time.
Staking in these fixed staking periods earns more rewards. The longer the period, the bigger the reward weight multiplier. Reward multiplier for different periods:
* Locked flexible period - **0.5**
* Flexible period - **1.0**
* 90 days - **1.1**
* 360 days - **1.5**
* 540 days - **2**
For locked tokens, only flexible staking is allowed and the reward multiplier is **0.5**. If a user delegates their locked tokens to a staking period, we will convert that to a flexible staking delegation.
After the staking period ends, users can choose not to unstake. In this case, they will continue earning the same reward rate based on the reward rate of the corresponding staking period until they unstake manually. They can unstake at any time after the staking period ends. For example, if the 1-year staking period's reward rate is 0.02% per block, after staking for 1 year, users can still earn 0.02% per block of the reward until they unstake.
## Decimal for stake amounts
The decimal for stake operations (stake, unstake, redelegate, etc.) is 9. If a user specifies a smaller value, the dust will be refunded back to the users. Or if there is no token transfer involved, the specified value will be rounded down to 9 decimals.
# Staking Operations
## Create validator
To become a validator, the validator must first run a validator node based on the latest released story binaries, then call the CreateValidator function with an initial staking amount, moniker, and commission rate. It also needs to set the max commission rate and max commission rate change to make sure it doesn't change the commission rate later dramatically. The minimum commission rate that a validator can set is 5%.
The initial staking amount needs to be larger than a threshold, which is 1024 IP. The amount will be deducted from the caller's wallet. It can only be staked to a flexible period.
If a validator tries to call create validator function the second time, it will be ignored.
## Update validator commission
This operation allows validators to edit their validator commission rate. If the updated commission rate is larger than max commission rate or the commission rate change delta is larger than max commission rate change, the operation will fail.
A fee of 1 IP will be charged for updating a validator to prevent spamming. The fee will be burnt by the contract.
The commission rate can only be updated once per day. It will not throw an error from the contract. But it won't take effect in the consensus layer.
## Stake
Both the validator and delegator can stake tokens to a validator. A validator can stake to itself, which is called self-delegation. Users can decide if they want to stake with a fixed staking period or stake without a period (flexible staking).
If a fixed period is chosen, a delegation id will be returned to the users. Users must use this delegation id to unstake tokens from this stake operation. If flexible staking is chosen, the returned delegation id will be 0.
The staking amount needs to be larger than a threshold, which is 1024 IP.
If a delegator delegates to a non-existent validator, the tokens will NOT be refunded.
If users specify the token amount that has more than 9 decimal units, the actual staking amount will be rounded down to 9 decimal and refund the remaining back to the users.
## Unstake
When staking without a staking period, users can unstake anytime. The tokens will be distributed to the user's account after the unbonding time.
A fee of 1 IP will be charged for unstaking to prevent spamming. The fee will be burnt by the contract.
When staking with a staking period, users can only unstake after the staking period is mature. The tokens will be distributed to the user's account after the unbonding time. Unstaking requests before the staking period matures will be ignored.
The minimum unstaking amount is 1024 IP. After the unstaking request is processed, if the remaining staked amount is less than 1024 IP, the remaining part will also be unstaked together.
The unstaking request will first go through the unbonding process, which is 14 days. After that, the unbonded requests are sent to a withdrawal queue, distributing a maximum of 32 withdrawals per block. If there are more than 32 withdrawal requests in the withdrawal queue, the next 32 withdrawal requests will be processed in the next block.
Partial unstake of a delegation is supported. For example, if a 1-year long delegation has 1 million tokens, after 1 year, users can unstake 500k from this delegation and keep the remaining staked to continue earning rewards.
Unstake can fail if the validator, delegator and delegation id passed in is incorrect.
Unstake can also fail if the maximum concurrent unbonding request (currently 14) has been reached for the validator/delegator pair.
If the unstake amount passed in is larger than the total unstakable tokens, the current total unstakable amounts will be unstaked. For example, if users unstake 1024 IP and only have 1023 IP stake, 1023 IP will be withdrawn.
If a validator exits, by either being offline and getting jailed, or not having enough stakes to be in the top 64 validator set, the delegators can unstake their tokens if the tokens are not in a staking period or their staking period is mature. Otherwise, delegators must wait until the staking period matures to unstake.
If users specify the token amount that has more than 9 decimal units, the actual unstaking amount will be rounded down to 9 decimal.
## Redelegate
Redelegate operation allows a delegator to move its staked tokens from one validator to another. The tokens can be redelegated to the new validator immediately and start earning rewards. However, the redelegated tokens are still subject to the unbonding process, IF the source validator is in the active validator set or unbonding from the active validator set. During this 14 days unbounding time, it will be slashed if the original validator gets slashed.
A fee of 1 IP will be charged for redelegation to prevent spamming. The fee will be burnt by the contract.
The minimum redelegation amount is 1024 IP. If a delegator's initial stake is 1024 IP but later gets slashed, it can still redelegate its tokens to another validator even if the token amount is less than 1024 IP.
Similarly to unstaking, if the redelegation amount passed in is larger than the total redelegatable tokens, the total redelegatable amounts will be redelegated. If the remaining balance after redelegation is less than 1024 IP, all remaining tokens will be redelegated together.
The delegation id will stay the same after the redelegation.
Redelegation has its own maximum ongoing unbonding transaction limit per delegator/source validator/destination validator pair, which is also 14.
Delegators can choose to redelegate their tokens to another active validator even if their tokens are still in an immature staking period. Their staking period maturation date and reward rate will stay the same.
Redelegation can only be triggered when the source and destination validators support the same token type.
If users specify the token amount that has more than 9 decimal units, the actual reledegated amount will be rounded down to 9 decimal.
## Set withdrawal/reward address
Delegators can call the staking contract to set a withdrawal address. The unstaked tokens will be sent to this withdrawal address. Similarly, delegators can set a separate reward address. All reward distributions will be sent to this address.
A fee of 1 IP will be charged for updating either the withdrawal address or the reward address to prevent spamming. The fee will be burnt by the contract.
The address change will take effect in the next block.
## Slash/Unjail
Slashing penalizes bad behaviors on the validators by slashing out a fraction of their staked tokens. Two types of behaviors can get slashed in Story: **double sign** and **downtime**.
* **double sign**: If a validator double signs for a block, they will get slashed 5% of their tokens and get permanently jailed (called tombstoned).
* **downtime**: If a validator is offline for too long and misses 95% of the past 28,800 blocks, they will get slashed 0.02% of their tokens and get jailed.
A validator will also get jailed after self-undelegation if the validator's remaining self-delegation amount is smaller than the minimum self-delegation (1024 IP).
A jailed validator cannot participate in the consensus and earn any reward. But they can unjail themselves after a cooldown time, which is currently set to 10 minutes. After 10 minutes, it can call story's staking contract to unjail itself IF their stake is more than minimum stake amount (1024 IP), after which it can participate in the consensus again if it's still within the top 64 validators.
A jailed validator can still withdraw all their stakes.
Delegators can still stake and unstake from a jailed validator as long as there are remaining stakes on this jailed validator. The jailed validator will only be removed from the chain (hence not able to be staked/unstaked) when there is no remaining stake on it.
A fee of 1 IP will be charged for unjailing a validator to prevent spamming. The fee will be burnt by the contract.
## On behalf functions
Most of the staking-related operations can be done from another wallet on behalf of the validators or delegators. Most of these on-behalf functions are permissionless since they spend tokens from the wallet that calls the on-behalf operations, not from the actual validators or delegators.
## Add operator
If a delegator wants to allow another wallet to unstake or redelegate on their behalf, they must call the staking contract to add that wallet as the operator for their delegator. After that, the operator can unstake and redelegate the delegator's tokens on behalf of the delegator.
The same applies to a validator who wants to allow another wallet to unjail on its behalf.
A fee of 1 IP will be charged for adding an operator.
## An additional data field
Each function will include an additional unformatted `data` input field to accommodate potential future changes. It can avoid changing user interfaces in the future.
## Validator key format
Validator public keys are secp256k1 keys. The keys have a 33 bytes compressed version and 65 bytes uncompressed version. When interacting with the story's smart contracts, a 33 bytes compressed key is used to identify validators.
# Rewards
## Rewards Pool Allocation
For every block, a fixed proportion of token inflation will go to the rewards distribution pool, which will be shared among all 64 active validators according to each of their share weights. *These allocated tokens will then be shared among the validator and its delegators in a fashion described by the next section.* The validator share weight is calculated based on the total token staking amount, and whether or not the token staking type is locked or unlocked.
As an example, assume that we have 100 tokens allocated for the validator rewards distribution pool, and assume that we only have 3 active validators:
* validatorA with 10 locked tokens staked
* validatorB with 10 locked tokens staked
* validatorC with 10 unlocked tokens staked
To calculate how many tokens each validator receives, we first calculate each of their weighted shares, which is defined as the number of staked tokens multiplied by their rewards multiplier (0.5 if staking locked tokens, 1 if staking unlocked tokens). This gives us:
* validatorA with 10 \* 0.5 = 5 shares
* validatorB with 10 \* 0.5 = 5 shares
* validatorC with 10 \* 1 = 10 shares
With the weighted and total shares calculated, we can then get the total number of inflationary tokens allocated for each validator:
* validatorA with 100 \* (5 / 20) = 25 tokens
* validatorB with 100 \* (5 / 20) = 25 tokens
* validatorC with 100 \* (10 / 20) = 50 tokens
The formula for calculating the total number of tokens allocated for a validator is as follows:
where
* R\_i is the total inflationary token rewards for validator i
* S\_i is the staked tokens for validator i
* M\_i is the rewards multiplier (0.5 for locked tokens, 1 for unlocked tokens)
* R\_total is the total inflationary tokens allocated for the rewards pool
## Validator And Delegator Rewards
Total rewards allocations (*whose calculations are shown in the prior section*) for each validator are shared between the validator itself and all of its delegators:
* The validator takes a fixed percentage commission, set by the validator itself
* Remaining rewards are distributed among delegators according to their share weights
Calculation of delegator rewards is similar to that of validator rewards, where the proportion of tokens received for each delegator out of the remaining validator rewards is calculated based on each delegator's staking multiplier (described in the staking section).
As an example, assume a validator has 100 total rewards allocated to it, with a validator commission of 20%, and 3 delegators delegating to it:
* delegatorA with 10 tokens staked and a staking multiplier of 1
* delegatorB with 10 tokens staked and a staking multiplier of 1
* delegatorC with 10 tokens staked and a staking multiplier of 2
To calculate how many tokens each delegator receives, we first calculate each of their weighted shares, which is defined as the number of staked tokens multiplied by their staking rewards multiplier. This gives us:
* delegatorA with 10 \* 1 = 10 shares
* delegatorB with 10 \* 1 = 10 shares
* delegatorC with 10 \* 2 = 20 shares
With the weighted and total shares calculated, we can then get the total number of inflationary tokens allocated for each delegator, noting that the total number of tokens to be distributed among delegators is give by 100 - (100 \* 0.20) = 80:
* delegatorA with 80 \* (10 / 40) = 20 tokens
* delegatorB with 80 \* (10 / 40) = 20 tokens
* delegatorC with 80 \* (20 / 40) = 40 tokens
The formula for calculating the delegator token reward can be found below:
where
* D\_i is the total inflationary token rewards for delegator i
* S\_i is the staked tokens for delegator i
* M\_i is the staked rewards multiplier for delegator i
* R\_total is the total inflationary tokens allocated for the validator
* C is the commission rate for the validator
The validator commission is also treated as a reward and will follow the same auto-reward distribution rule described below. The minimal validator commission is set to 5% to avoid a cut-throat competition of lower commission rates among validators.
The reward calculation results will be rounded down to gwei. Anything smaller than 1 gwei will be truncated.
## Auto reward distribution
The reward is accumulated per block and can be distributed per block. However, it will only be automatically distributed to the delegator's account when it is larger than a threshold. The default and also minimal threshold is 8 IP, which means that only if the delegator's reward is more than 8 IP, it will be sent to the delegator's account.
The reward distribution will go to a reward distribution queue, which only processes a fixed amount of reward distribution requests per block. The reward distribution per block is 32.
The staking reward cannot be manually withdrawn by design.
# Community Pool
A percentage of the newly minted tokens in every block will go to a community pool contract. The foundation will determine how to use the tokens sent to the pool. The maximum community pool percentage that can be set is 20%.
The community pool contract address: **0xcccccc0000000000000000000000000000000002**
# Singularity
The first 1,580,851 blocks after the genesis is called Singularity, during which everyone can create a validator and stake tokens but the active validator set will only have the genesis validators. There is also no new token emission, hence no reward. Unstake and redelegate are also not supported.
The Genesis validator set consists of 8 validators, setup by the foundation and trusted staking institutions. 4 of them support locked tokens and the other 4 support unlocked tokens. Each of them has an initial stake of 0.001 IP. Each of them will set a commission rate. During the Singularity, the genesis valdiators will need to self delegate at least 1024 IP to perform validator operations like editing validator commission rate.
After Singularity, the top 64 validator nodes with the highest stakes will be selected to participate in consensus and receive rewards.
Slashing/Jail won't happen during Singularity.
# Staking contract
Story's staking contract will handle all validators/delegators related operations. It's deployed to address: **0xcccccc0000000000000000000000000000000001**
The contract interfaces are defined here: [https://github.com/piplabs/story/blob/main/contracts/src/protocol/IPTokenStaking.sol](https://github.com/piplabs/story/blob/main/contracts/src/protocol/IPTokenStaking.sol)
# Whitepaper
Source: https://docs.story.foundation/network/learn/whitepaper
# Welcome to Story Network
Source: https://docs.story.foundation/network/overview
**Story** is a purpose-built Layer 1 blockchain transforming how intellectual property (IP) is registered, managed, and monetized in the digital era. Through its full EVM compatibility and optimized execution layer, Story handles complex IP data structures efficiently—delivering high speed at low cost.
## Getting Started
Connect and use the network.
Learn more about the architecture.
Become a validator.
Build applications on Story.
# Forum
Source: https://docs.story.foundation/network/participate/forum
# Governance
Source: https://docs.story.foundation/network/participate/governance
# Participate
Source: https://docs.story.foundation/network/participate/overview
There are many ways to participate in the Story Protocol ecosystem. Choose from the options below to get started.
Join the network as a validator and help secure the Story Protocol blockchain
Participate in protocol governance and help shape the future of Story Protocol
Contribute to Story Improvement Proposals and protocol development
Join discussions and connect with the Story Protocol community
# SIP Repo
Source: https://docs.story.foundation/network/participate/sip
# DKG Validator Guide
Source: https://docs.story.foundation/network/participate/validators/dkg/dkg-validator-guide
Guide to setting up and running story-kernel for DKG committee participation
DKG is currently only available on the **Aeneid testnet**. Mainnet support will follow in a future release.
## Overview
Starting from the v1.6.0 upgrade, validators can participate in the DKG (Distributed Key Generation) committee. Participation requires running [**story-kernel**](https://github.com/piplabs/story-kernel) — a TEE client that executes inside an Intel SGX enclave alongside your validator node.
DKG participation is **optional**. You can continue running a validator without joining the DKG committee by keeping `dkg.enable = false` in your `story.toml`.
**What to know before joining:**
* **SGX hardware required** — your machine must have Intel SGX support
* **Self-undelegation is blocked** while you are an active DKG committee member. You cannot unstake your own delegation until the current DKG round ends (\~7 days with production parameters). Other delegators are not affected.
* If your kernel goes down, your validator continues producing blocks normally. If the kernel restarts and the node finalizes successfully before the current round ends, it can rejoin that round; otherwise, it rejoins on the next one.
## Hardware Requirements
story-kernel runs inside an SGX enclave that requires dedicated Enclave Page Cache (EPC) memory. The Gramine manifest configures a **4 GB enclave** for the Go runtime and DKG cryptographic operations.
| Resource | Minimum | Recommended | Notes |
| ---------- | ------------------ | ----------- | ----------------------------------------------------------- |
| CPU | 2 cores, Intel SGX | 4+ cores | Xeon Platinum 8370C (Ice Lake-SP) or newer |
| RAM | 8 GB | 16 GB | Enclave uses 4 GB EPC; host needs the rest for story + geth |
| EPC Memory | 4 GB | 8 GB | Must be ≥ enclave\_size (4 GB) |
| Disk | 50 GB | 128 GB+ | Kernel data is small (\~100 MB) |
**Supported cloud instances (Azure):**
| Instance Type | vCPUs | RAM | EPC | Notes |
| -------------------- | ----- | ------- | ------ | ---------------------------------------------- |
| Standard\_DC1s\_v3 | 1 | 8 GB | 4 GB | Meets minimum EPC but tight on RAM |
| Standard\_DC2s\_v3 | 2 | 16 GB | 8 GB | Minimum recommended |
| Standard\_DC4s\_v3 | 4 | 32 GB | 16 GB | Recommended |
| Standard\_DC8s\_v3 | 8 | 64 GB | 32 GB | High-load validators |
| Standard\_DC16s\_v3+ | 16+ | 128+ GB | 64+ GB | Up to DC48s\_v3 (48 vCPUs, 384 GB, 256 GB EPC) |
**Bare metal** is also supported — any Intel server with SGX enabled in BIOS. Check EPC size with `dmesg | grep "sgx: EPC section"`.
AMD SEV-SNP instances (e.g., Azure DCasv5) and ARM instances are **not supported** at the moment.
## Software Requirements
All validators **must** use the exact same versions below to produce identical MRENCLAVE (code commitment) values.
| Component | Required Version | Why |
| --------- | ---------------- | ------------------------------------------------ |
| Ubuntu | 24.04 LTS | Library paths are measured into MRENCLAVE |
| Go | 1.24.0 | Different versions produce different binaries |
| Gramine | 1.9 | Must be installed via apt, not built from source |
***
## Setup Guide
### Step 1: Verify SGX Support
```bash theme={null}
ls /dev/sgx_enclave && echo "SGX available" || echo "SGX NOT available"
```
If `/dev/sgx_enclave` does not exist, SGX is not supported or not enabled in BIOS/cloud settings.
### Step 2: Install Dependencies
#### Intel SGX SDK and DCAP
```bash theme={null}
sudo mkdir -p /etc/apt/keyrings
wget -qO- https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key \
| sudo tee /etc/apt/keyrings/intel-sgx-keyring.asc > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/intel-sgx-keyring.asc arch=amd64]\
https://download.01.org/intel-sgx/sgx_repo/ubuntu noble main" \
| sudo tee /etc/apt/sources.list.d/intel-sgx.list
sudo apt update
sudo apt install -y build-essential cmake libssl-dev \
libsgx-dcap-default-qpl libsgx-enclave-common libsgx-quote-ex
```
#### Configure PCCS
Edit `/etc/sgx_default_qcnl.conf`:
```json theme={null}
{
"pccs_url": "https://global.acccache.azure.net/sgx/certification/v4/",
"collateral_service": "https://global.acccache.azure.net/sgx/certification/v4/"
}
```
#### Install Gramine 1.9
```bash theme={null}
sudo curl -fsSLo /usr/share/keyrings/gramine-keyring.gpg \
https://packages.gramineproject.io/gramine-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/gramine-keyring.gpg]\
https://packages.gramineproject.io/ noble main" \
| sudo tee /etc/apt/sources.list.d/gramine.list
sudo apt update
sudo apt install -y gramine=1.9
```
### Step 3: Build story-kernel
```bash theme={null}
git clone https://github.com/piplabs/story-kernel.git
cd story-kernel
git checkout # use the tag from the upgrade announcement
make setup-cbmpc # first time only
make build-with-cpp
make all-gramine
```
Note the **MRENCLAVE** value from the output:
```
Code Commitment: mr_enclave: <64-char hex>
```
All validators must produce the **same MRENCLAVE**. If yours differs, verify you are on the exact same commit, OS version, Go version, and Gramine version.
### Step 4: Set Up Data Directory
```bash theme={null}
sudo mkdir -p /opt/story-kernel
sudo chown $USER:$USER /opt/story-kernel
```
### Step 5: Initialize and Configure
#### Initialize
```bash theme={null}
gramine-sgx story-kernel init --home /opt/story-kernel
```
#### Configure
Edit `/opt/story-kernel/config.toml`:
```toml theme={null}
log-level = "info"
[grpc]
listen_addr = ":50051"
[light_client]
chain_id = "devnet-1"
rpc_addr = "http://localhost:26657"
primary_addr = "http://localhost:26657"
witness_addrs = ["http://:26657", "http://:26657"]
trusted_height =
trusted_hash = ""
```
The **trusted block must be within the last 2 weeks**. The light client uses a trust period — if the trusted block is older, header verification will fail.
Get a recent trusted block:
```bash theme={null}
curl -s 'http://localhost:26657/block' | python3 -c "
import json, sys
r = json.load(sys.stdin)['result']
print(f'trusted_height = {r[\"block\"][\"header\"][\"height\"]}')
print(f'trusted_hash =\"{r[\"block_id\"][\"hash\"]}\"')
"
```
| Field | Description |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chain_id` | CL chain ID: `devnet-1` for Aeneid |
| `witness_addrs` | At least 2 CometBFT RPC endpoints for light client cross-validation. The same address can be repeated (e.g., `["http://x:26657", "http://x:26657"]`), but using 2 different validators is recommended for better security. Use internal/private IPs if on the same network. |
### Step 6: Configure Story Client
Apply the following changes to `~/.story/story/config/story.toml`:
#### Add `engine-chain-id` (if not present)
```toml theme={null}
engine-chain-id = 1315 # for Aeneid
```
#### Add DKG Options section
```toml theme={null}
#######################################################################
### DKG Options ###
#######################################################################
[dkg]
# Enable defines if the DKG client is enabled or not.
enable = true
# Comma-separated list of story-kernel (TEE) endpoints.
kernel-endpoints = ["127.0.0.1:50051"]
# The RPC endpoint of execution layer.
engine-rpc-endpoint = "http://127.0.0.1:8545"
# TEE enclave type identifier (1 for SGX).
enc-type = 1
# TLS configuration for kernel gRPC connections (optional).
#kernel-tls-ca-file = "/path/to/ca.crt"
#kernel-tls-cert-file = "/path/to/client.crt"
#kernel-tls-key-file = "/path/to/client.key"
```
### Step 7: Start Services
**Start order matters.** Start story-kernel AFTER the chain is running — the kernel needs CometBFT RPC (port 26657) for light client initialization.
#### Start story-kernel
```bash theme={null}
sudo tee /etc/systemd/system/story-kernel.service > /dev/null <
```
#### DKG registration
```bash theme={null}
journalctl -u story --no-pager | grep "DKG_REG_STATUS_VERIFIED"
```
***
## Troubleshooting
### Kernel won't start
| Error | Fix |
| --------------------------------------- | ------------------------------------------------------------------------------- |
| `/dev/sgx_enclave: No such file` | Enable SGX in BIOS or use an SGX-capable VM |
| Stuck at "Parsing TOML manifest file" | Normal — wait 1-3 minutes |
| `config not found` | Ensure config exists at `/opt/story-kernel/config.toml` |
| `height requested is too high` | Delete `/opt/story-kernel/light_client/` and restart with a fresh trusted block |
| `at least 2 witness addresses required` | Add at least 2 entries to `witness_addrs` in config |
### DKG registration fails
| Error | Fix |
| -------------------------------------- | --------------------------------------------------------------------------------------- |
| `no kernel client for code commitment` | Check story logs for `Connected to kernel`; verify MRENCLAVE matches on-chain whitelist |
| `execution reverted` | DCAP attestation failed — check SGX setup and PCCS config |
| `Failed to generate the sealed key` | Check kernel logs for details |
### Light client issues
```bash theme={null}
sudo systemctl stop story-kernel
rm -rf /opt/story-kernel/light_client/
# Update trusted_height and trusted_hash in config.toml
sudo systemctl start story-kernel
```
***
## Important Notes
### Self-undelegation restriction
While your validator is a **finalized member of the active DKG round**, self-undelegation is blocked. This prevents committee members from leaving mid-round, which could compromise threshold cryptography.
* Only **self-undelegation** is blocked — other delegators can unstake normally
* The restriction lifts when the current round ends
* A full DKG round with production parameters takes approximately **7 days**
### Running kernel on a separate machine
story-kernel can run on a dedicated SGX machine. Update `kernel-endpoints` in `story.toml`:
```toml theme={null}
kernel-endpoints = [":50051"]
```
The SGX machine needs network access to your CometBFT RPC (port 26657).
***
## Additional Information
### Build Environment
| Component | Version |
| ------------------ | ------------ |
| Ubuntu | 24.04.4 LTS |
| Go | 1.24.0 |
| Gramine | 1.9 (apt) |
| gramine-ratls-dcap | 1.9 |
| story-kernel | v0.1.0 (tag) |
| cb-mpc | v0.0.1-alpha |
### Enclave Measurement
The measurements below correspond to `story-kernel` release `v0.1.0`. They can change when `story-kernel` or its measured build inputs are upgraded.
```
MRENCLAVE: 6b2fb25e0084ad6ecbf6cfcefe09e2fa0fca2b84092f72c01f8fe98e9d7db5cd
Binary hash: 320a744e87426e36de63ebc8c7ff3af23e00ac7b29ad6093acdafd0bb029ca36
```
***
## FAQ
**Do I need SGX to run a validator?**
No. SGX is only needed for DKG committee participation. Set `dkg.enable = false` to run without it.
**What happens if my kernel goes down?**
Your validator continues producing blocks. If the kernel restarts and the node finalizes successfully before the current DKG round ends, it can rejoin that round; otherwise, it rejoins on the next one.
**What is MRENCLAVE?**
A cryptographic hash of the SGX enclave contents. All validators must produce the same value to participate in the same DKG committee.
**Can I opt out after joining?**
Yes. Set `dkg.enable = false` and restart story. You stop participating after the current round ends.
# Kernel Upgrade
Source: https://docs.story.foundation/network/participate/validators/dkg/kernel-upgrade
Guide to upgrading the story-kernel binary on DKG committee validators
## Kernel Upgrade Workflow (Aeneid)
Upgrade the story-kernel binary on Aeneid testnet DKG committee validators.
This uses the DKG on-chain upgrade mechanism: whitelist new MRENCLAVE, schedule upgrade, dual-kernel resharing, then cutover.
***
## Prerequisites
* It is recommended to start the upgrade when current DKG round is in **Active** stage
* New story-kernel binary built on all validator machines (must produce identical MRENCLAVE)
* Timelock/owner access to DKG contract for `whitelistEnclaveType` and `scheduleUpgrade`
* SGXValidationHook proxy address known for the new kernel client
## Phase 1: Build New Kernel
Build the new story-kernel binary **on each validator machine** (never SCP binaries — MRENCLAVE must match).
```bash theme={null}
cd ~/story-kernel
git pull origin release/0.1
make clean && make build-with-cpp && make all-gramine
NEW_MRENCLAVE=$(cat story-kernel.manifest.sgx.d/mrenclave.txt)
echo "New MRENCLAVE: $NEW_MRENCLAVE"
```
Verify all validators produce the **same** `NEW_MRENCLAVE` value before proceeding.
## Phase 2: Start Dual Kernels
The new kernel runs alongside the old kernel on a separate port. Story CL identifies each kernel by its `code_commitment` (MRENCLAVE).
```bash theme={null}
# Old kernel: already running on :50051
# New kernel: start on :50052 with separate home dir
# Verify both are listening
sudo lsof -i :50051 | grep LISTEN # old
sudo lsof -i :50052 | grep LISTEN # new
```
The new kernel needs its own:
* Home directory (separate light client state)
* Gramine manifest with different `listen_addr` (`:50052`)
## Phase 3: Update Story Config + Restart
Add the new kernel endpoint to `story.toml`:
```toml theme={null}
kernel-endpoints = ["127.0.0.1:50051", "127.0.0.1:50052"]
```
Restart story:
```bash theme={null}
sudo systemctl restart story
```
Verify **both** kernels connected:
```bash theme={null}
journalctl -u story --since '1 minute ago' | grep "Connected to kernel"
# Must see TWO entries with different code_commitment values
# Verify: connected_clients=2
```
## Phase 4: Whitelist + Schedule Upgrade On-Chain
Wait for the current DKG round to be in **Active** stage, then:
```bash theme={null}
DKG="0xCcCcCC0000000000000000000000000000000004"
ENCLAVE_TYPE="0x0000000000000000000000000000000000000000000000000000000000000001"
SGX_HOOK=""
# 1. Whitelist new MRENCLAVE
cast send $DKG 'whitelistEnclaveType(bytes32,(bytes32,address),bool)' \
$ENCLAVE_TYPE "(0x$NEW_MRENCLAVE,$SGX_HOOK)" true \
--rpc-url $RPC --private-key $KEY --legacy --gas-price 30000000000
# 2. Schedule upgrade (activation = current height + buffer)
CURRENT=$(cast block-number --rpc-url $RPC)
ACTIVATION=$((CURRENT + 50))
cast send $DKG 'scheduleUpgrade(uint256,string)' $ACTIVATION "v" \
--rpc-url $RPC --private-key $KEY --legacy --gas-price 30000000000
```
On Aeneid, DKG contract ops go through Timelock (minDelay=600s).
Schedule the Timelock tx, wait 10 min, then execute.
## Phase 5: Wait for Upgrade Resharing
```bash theme={null}
# Monitor activation
while true; do
HEIGHT=$(curl -s localhost:26657/status | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['sync_info']['latest_block_height'])")
echo "Height: $HEIGHT / $ACTIVATION"
if [ "$HEIGHT" -ge "$ACTIVATION" ]; then break; fi
sleep 5
done
# Verify upgrade resharing round started
journalctl -u story --since '5 minutes ago' | grep 'is_upgrade.*true'
# Wait for completion
timeout 3600 bash -c "while ! journalctl -u story --since '1 hour ago' | grep -q 'DKG finalization phase complete'; do sleep 15; done"
```
## Phase 6: Cutover to New Kernel
After upgrade resharing completes successfully:
```bash theme={null}
# 1. Stop old kernel
sudo systemctl stop story-kernel # old on :50051
# 2. Update story.toml to only new kernel
sed -i 's|kernel-endpoints = \["127.0.0.1:50051", "127.0.0.1:50052"\]|kernel-endpoints = ["127.0.0.1:50052"]|' \
~/.story/story/config/story.toml
# 3. Restart story
sudo systemctl restart story
# 4. Verify
journalctl -u story --since '1 minute ago' | grep "Connected to kernel"
# Should see 1 entry with the NEW code_commitment
```
***
## Verification Checklist
* [ ] All validators built identical `NEW_MRENCLAVE`
* [ ] Both kernels connected on all validators (`connected_clients=2`)
* [ ] `whitelistEnclaveType` tx confirmed (new MRENCLAVE on enclave type 1)
* [ ] `scheduleUpgrade` tx confirmed with target activation height
* [ ] Upgrade resharing round initiated with `is_upgrade=true`
* [ ] Old kernel generates deals, new kernel processes responses
* [ ] `DKG finalization phase complete` on all committee members
* [ ] Old kernel stopped, config updated to new kernel only
* [ ] New DKG round proceeds normally on new kernel
## Troubleshooting
| Symptom | Cause | Fix |
| ------------------------------------------------ | --------------------------------------- | ------------------------------------------------------- |
| `connected_clients=1` after restart | New kernel not running or port mismatch | Verify `lsof -i :50052`, check Gramine manifest |
| "no new kernel client found for upgrade" | Story not connected to new kernel | Ensure `kernel-endpoints` has both ports, restart story |
| Upgrade round doesn't start at activation height | Not in Active stage when scheduled | Reschedule during next Active stage |
| Finalization fails | Insufficient committee members upgraded | Ensure all validators have dual kernels running |
# Full Node
Source: https://docs.story.foundation/network/participate/validators/node-setup-mainnet
Guide to setting up a Story node for mainnet
This section will guide you through how to setup a Story node for mainnet. Story draws inspiration from ETH PoS in decoupling execution and consensus clients. The execution client `story-geth` relays EVM blocks into the `story` consensus client via Engine API, using an ABCI++ adapter to make EVM state compatible with that of CometBFT. With this architecture, consensus efficiency is no longer bottlenecked by execution transaction throughput.
The `story` and `geth` binaries, which make up the clients required for running Story nodes, are available from our latest `release` pages:
| Network | story-geth | story |
| ------- | ----------------- | ---------------- |
| Mainnet | v1.2.0 (Yasunari) | v1.4.2 (Terence) |
| Aeneid | v1.2.0 (Yasunari) | v1.4.2 (Terence) |
* **`story-geth`execution client:**
* Release Link: [**Click here**](https://github.com/piplabs/story-geth/releases)
* Latest Stable Binary (v1.2.0): [**Click here**](https://github.com/piplabs/story-geth/releases/tag/v1.2.0)
* **`story-geth`execution client:** (For Aeneid testnet)
* Release Link: [**Click here**](https://github.com/piplabs/story-geth/releases)
* Latest Stable Binary (v1.2.0): [**Click here**](https://github.com/piplabs/story-geth/releases/tag/v1.2.0)
* **`story`consensus client:**
* Releases link: [**Click here**](https://github.com/piplabs/story/releases)
* Latest Stable Binary (v1.4.2): [**Click here**](https://github.com/piplabs/story/releases/tag/v1.4.2)
* **`story`consensus client:** (For Aeneid testnet)
* Releases link: [**Click here**](https://github.com/piplabs/story/releases)
* Latest Stable Binary (v1.4.2): [**Click here**](https://github.com/piplabs/story/releases/tag/v1.4.2)
# Story Node Installation Guide
## Pre-Installation Checklist
* [ ] Verify system meets hardware requirements
* [ ] Operating system: Ubuntu 22.04 LTS
* [ ] Required ports are available
* [ ] Sufficient disk space available
* [ ] Root or sudo access
## Quick Reference
* Installation time: \~30 minutes
* Network: Story Mainnet or Story Aeneid Testnet
* Required versions:
* Check Latest Release
## 1. System Preparation
### 1.1 System Requirements
For optimal performance and reliability, we recommend running your node on either:
* A Virtual Private Server (VPS)
* A dedicated Linux-based machine
### System Specs
| Hardware | Minimal Requirement |
| --------- | ------------------- |
| CPU | Dedicated 8 Cores |
| RAM | 32 GB |
| Disk | 500 GB NVMe Drive |
| Bandwidth | 25 MBit/s |
### 1.2 Required Ports
*Ensure all ports needed for your node functionality are needed, described below*
* `story-geth`
* 8545
* Required if you want your node to interface via JSON-RPC API over HTTP
* 8546
* Required for websockets interaction
* 30303 (TCP + API)
* MUST be open for p2p communication
* `story`
* 26656
* MUST be open for consensus p2p communication
* 26657
* Required if you want your node interfacing for Tendermint RPC
* 26660
* Needed if you want to expose prometheus metrics
### 1.3 Install Dependencies
```bash theme={null}
# Update system
sudo apt update && sudo apt-get update
# Install required packages
sudo apt install -y \
curl \
git \
make \
jq \
build-essential \
gcc \
unzip \
wget \
lz4 \
aria2 \
gh
```
### 1.4 Install Go
For Odyssey, we need to install Go 1.22.0
```bash theme={null}
# Download and install Go 1.22.0
cd $HOME
# Set Go version
GO_VERSION="1.22.0"
# Download Go binary
wget "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz"
# Remove existing Go installation and extract new version
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz"
# Clean up downloaded archive
rm "go${GO_VERSION}.linux-amd64.tar.gz"
# Add Go to PATH
echo "export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin" >> ~/.bash_profile
source ~/.bash_profile
# Verify installation
go version
```
## 2. Story Node Installation
### 2.1 Install Story-Geth
1. Download and setup binary
```bash theme={null}
cd $HOME
wget https://github.com/piplabs/story-geth/releases/download/v1.2.0/geth-linux-amd64
sudo mv ./geth-linux-amd64 story-geth
sudo chmod +x story-geth
sudo mv ./story-geth $HOME/go/bin/
source $HOME/.bashrc
# Verify installation
story-geth version
```
You will see the version of the geth binary.
```
Geth
version: 1.2.0-stable
```
(Mac OS X only) The OS X binaries have yet to be signed by our build process, so you may need to unquarantine them manually:
```bash theme={null}
sudo xattr -rd com.apple.quarantine ./geth
```
2. Configure and start service
```bash theme={null}
# Setup systemd service
sudo tee /etc/systemd/system/story-geth.service > /dev/null <
```bash theme={null}
# Setup systemd service
sudo tee /etc/systemd/system/story-geth.service > /dev/null <
### 2.2 Install Story Consensus Client
#### Cosmovisor installation
For updating the story client, we recommend using Cosmovisor.
1. Install Cosmovisor
```bash theme={null}
go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@v1.6.0
cosmovisor version
```
2. Configure Cosmovisor
```bash theme={null}
# Set daemon configuration
export DAEMON_NAME=story
export DAEMON_HOME=$HOME/.story/story
export DAEMON_DATA_BACKUP_DIR=${DAEMON_HOME}/cosmovisor/backup
sudo mkdir -p \
$DAEMON_HOME/cosmovisor/backup \
$DAEMON_HOME/data
# Persist configuration
echo "export DAEMON_NAME=story" >> $HOME/.bash_profile
echo "export DAEMON_HOME=$HOME/.story/story" >> $HOME/.bash_profile
echo "export DAEMON_DATA_BACKUP_DIR=${DAEMON_HOME}/cosmovisor/backup" >> $HOME/.bash_profile
echo "export DAEMON_ALLOW_DOWNLOAD_BINARIES=false" >> $HOME/.bash_profile
```
#### Install Story Client
```bash theme={null}
cd $HOME
wget https://github.com/piplabs/story/releases/download/v1.4.2/story-linux-amd64
sudo mv story-linux-amd64 story
sudo chmod +x story
sudo mv ./story $HOME/go/bin/
source $HOME/.bashrc
story version
```
> You should expect to see version 1.4.2-stable
(Mac OS X Only) The OS X binaries have yet to be signed by our build process, so you may need to unquarantine them manually:
```bash theme={null}
sudo xattr -rd com.apple.quarantine ./story
```
#### Init Story with Cosmovisor
```bash theme={null}
cosmovisor init ./story
cosmovisor run init --network story --moniker ${moniker_name}
cosmovisor version
```
```bash theme={null}
cosmovisor init ./story
cosmovisor run init --network aeneid --moniker ${moniker_name}
cosmovisor version
```
#### Custom Configuration
To override your own node settings, you can do the following:
* `${STORY_DATA_ROOT}/config/config.toml` can be modified to change network and consensus settings
* `${STORY_DATA_ROOT}/config/story.toml` to update various client configs
* `${STORY_DATA_ROOT}/priv_validator_key.json` is a sensitive file containing your validator key, but may be replaced with your own
#### Custom Automation
Below we list a sample `Systemd` configuration you may use on Linux
The Story API endpoint (`--api-address`) can be modified as needed depending on your environment or use case.
```bash theme={null}
# story
sudo tee /etc/systemd/system/story.service > /dev/null <
```bash theme={null}
# story
sudo tee /etc/systemd/system/story.service > /dev/null <
```bash theme={null}
# story
sudo tee /etc/systemd/system/story.service > /dev/null <
#### Start the service
```bash theme={null}
sudo systemctl daemon-reload
sudo systemctl enable story
sudo systemctl start story
# Monitor logs
journalctl -u cosmovisor -f -o cat
```
#### Debugging
If you would like to check the status of `story` while it is running, it is helpful to query its internal JSONRPC/HTTP endpoint. Here are a few helpful commands to run:
* `curl localhost:26657/net_info | jq '.result.peers[].node_info.moniker'`
* This will give you a list of consesus peers the node is sync'd with by moniker
* `curl localhost:26657/health`
* This will let you know if the node is healthy - `{}` indicates it is
## 3. Verify Installation
### 3.1 Check Geth Status
```bash theme={null}
# Check sync status
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://localhost:8545
```
### 3.2 Check Consensus Client
```bash theme={null}
# Check node status
curl localhost:26657/status
# Check peer connections
curl localhost:26657/net_info | jq '.result.peers[].node_info.moniker'
```
## Clean status
If you ever run into issues and would like to try joining the network from a cleared state, run the following:
### Geth
```bash theme={null}
rm -rf ${GETH_DATA_ROOT} && ./geth --story --syncmode full
```
Mac OS X: `rm -rf ~/Library/Story/geth/* && ./geth --story --syncmode full`
Linux: `rm -rf ~/.story/geth/* && ./geth --story --syncmode full`
```bash theme={null}
rm -rf ${GETH_DATA_ROOT} && ./geth --aeneid --syncmode full
```
Mac OS X: `rm -rf ~/Library/Story/geth/* && ./geth --aeneid --syncmode full`
Linux: `rm -rf ~/.story/geth/* && ./geth --aeneid --syncmode full`
### Story
```bash theme={null}
rm -rf ${STORY_DATA_ROOT} && ./story init --network story && ./story run
```
Mac OS X: `rm -rf ~/Library/Story/story/* && ./story init --network story && ./story run`
Linux: `rm -rf ~/.story/story/* && ./story init --network story && ./story run`
```bash theme={null}
rm -rf ${STORY_DATA_ROOT} && ./story init --network aeneid && ./story run
```
Mac OS X: `rm -rf ~/Library/Story/story/* && ./story init --network aeneid && ./story run`
Linux: `rm -rf ~/.story/story/* && ./story init --network aeneid && ./story run`
# Node Upgrade
Source: https://docs.story.foundation/network/participate/validators/node-upgrade
Guide to upgrading your Story node clients
There are three types of upgrades
1. Upgrade the story geth client
2. Upgrade the story client manually
3. Schedule the upgrade with Cosmovisor
### Upgrade the story geth client
```bash theme={null}
# Stop the services
sudo systemctl stop story
sudo systemctl stop story-geth
# Download the new binary
wget ${STORY_GETH_BINARY_URL}
sudo mv ./geth-linux-amd64 story-geth
sudo chmod +x story-geth
sudo mv ./story-geth $HOME/go/bin/story-geth
source $HOME/.bashrc
# Restart the service
sudo systemctl start story-geth
sudo systemctl start story
```
### Upgrade the story client manually
```bash theme={null}
# Stop the service
sudo systemctl stop story
# Download the new binary
wget ${STORY_BINARY_URL}
sudo mv story-linux-amd64 story
sudo chmod +x story
sudo mv ./story $HOME/go/bin/story
# Schedule the update
sudo systemctl start story
```
### Schedule the upgrade with Cosmovisor
The following steps outline how to schedule an upgrade using Cosmovisor:
1. Create the upgrade directory and download the new binary
```bash theme={null}
# Download the new binary
wget ${STORY_BINARY_URL}
# Schedule the upgrade
source $HOME/.bash_profile
cosmovisor add-upgrade ${UPGRADE_NAME} ${UPGRADE_PATH} \
--force \
--upgrade-height ${UPGRADE_HEIGHT}
```
2. Verify the upgrade configuration
```bash theme={null}
# Check the upgrade info
cat $HOME/.story/story/data/upgrade-info.json
```
The upgrade-info.json should show:
```json theme={null}
{
"name": "v1.0.0",
"time": "2025-02-05T12:00:00Z",
"height": 858000
}
```
3. Monitor the upgrade
```bash theme={null}
# Watch the node logs for the upgrade
journalctl -u story -f -o cat
```
Note: Cosmovisor will automatically handle the binary switch once the specified block height is reached. Before the upgrade, confirm that your node is fully synced and has enough disk space available.
### Use Cosmovisor While running Story Node
This guide is for people who are running story without using cosmovisor, but still want to use cosmovisor to schedule the upgrade.
1. Install Cosmovisor
```bash theme={null}
# Install Cosmovisor
go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@1.6.0
cosmovisor version
```
You will see the version of cosmovisor.
```
cosmovisor version: v1.6.0
Error: failed to run version command: DAEMON_NAME is not set
DAEMON_HOME is not set
DAEMON_DATA_BACKUP_DIR must not be empty
```
2. Set the environment variables
```bash theme={null}
# Set daemon configuration
export DAEMON_NAME=story
export DAEMON_HOME=$HOME/.story/story
export DAEMON_DATA_BACKUP_DIR=${DAEMON_HOME}/cosmovisor/backup
sudo mkdir -p $DAEMON_HOME/cosmovisor/backup $DAEMON_HOME/data
# Persist configuration
echo "export DAEMON_NAME=story" >> $HOME/.bash_profile
echo "export DAEMON_HOME=$HOME/.story/story" >> $HOME/.bash_profile
echo "export DAEMON_DATA_BACKUP_DIR=${DAEMON_HOME}/cosmovisor/backup" >> $HOME/.bash_profile
echo "export DAEMON_ALLOW_DOWNLOAD_BINARIES=false" >> $HOME/.bash_profile
```
If you have any permission issues, you can run the following command to fix it.
```bash theme={null}
sudo chown -R $USER:$USER $HOME/.story
```
3. Setup the cosmovisor
```bash theme={null}
# Create the cosmovisor directory
mkdir -p $HOME/.story/cosmovisor/genesis/bin
# Copy the new binary to the cosmovisor directory
cp $HOME/go/bin/story $HOME/.story/cosmovisor/genesis/bin/
```
4. Add cosmovisor to the systemd service
```bash theme={null}
sudo tee /etc/systemd/system/cosmovisor.service > /dev/null <
Download the latest Story Geth client releases
Download the latest Story consensus client releases
# Overview
This section will guide you through how you can run your own validator. Validator operations may be done via the `story` consensus client.
The below operations do not require running a node! However, if you would like
to participate in staking rewards, you must run a validator node.
Before proceeding, it is important to familiarize yourself with the difference between a delegator and a validator:
* A **validator** is a full node that participates in consensus whose signed key resides in the `priv_validator_key.json` file under your `story` data directory. To print out your validator key details you may refer to the [validator key export section](/network/become-a-validator#validator-key-export)
* A **delegator** refers to an account operator that holds `IP` and wishes to participate in consensus rewards but without needing to run a validator themselves.
In the same folder as where your `story` binary resides, add a `.env` file with a `PRIVATE_KEY` whose account has `IP` funded. **We recommend using your delegator account for all below operations.**
You may also issue transactions as the validator itself. To get the EVM
private key corresponding to your validator, please refer to the [Validator
Key Export](#validator-key-export) section. From **Story v1.2.0**, user must
use `.env` for all operations.
The `.env` file should look like the following *(make sure not to add a 0x prefix):*
```bash theme={null}
# ~/.env
PRIVATE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
With this, you are all set to perform different validator operations! Below, we will guide you through all of those supported via the CLI:
## Validator Key Export
By default, when you run `./story init` a validator key is created for you. To view your validator key, run the following command:
```bash theme={null}
./story validator export [flags]
```
This will print out your validator public key file in compressed and uncompressed formats. By default, we use the hex-encoded compressed key for public identification.
```text theme={null}
Compressed Public Key (hex): 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984
Compressed Public Key (base64): A73HuJQLq+kibVLX+imaH689ZKgvgJiJJWyPFGlYpjmE
Uncompressed Public Key (hex): 04bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a6398496b9e2af0a3a1d199c3cc1d09ee899336a530c185df6b46a9735b25e79a493af
EVM Address: 0x9EacBe2C3B1eb0a9FC14106d97bd3A1F89efdDCc
Validator Address: storyvaloper1p470h0jtph4n5hztallp8vznq8ehylsw9vpddx
Delegator Address: story1p470h0jtph4n5hztallp8vznq8ehylswtr4vxd
```
**Available Flags:**
* `--export-evm-key`: (bool) Exports the derived EVM private key of your validator into the default data config directory
* `--export-evm-key-path`: (string) Specifies a different download location for the derived EVM private key of your validator
* `--keyfile`: (string) Path to the Tendermint key file (default "/home/ubuntu/.story/story/config/priv\_validator\_key.json")
If you would like to issue transactions as your validator, and not as a
delegator, you may export the key to your `.env` file and ensure it has IP
sent to it, e.g. via `./story validator export --export-evm-key --evm-key-path
.env`
## Validator Creation
To create a new validator, run the following command:
```bash Locked token node theme={null}
./story validator create
--stake ${AMOUNT_TO_STAKE_IN_WEI} \
--moniker ${VALIDATOR_NAME} \
--rpc ${rpc} \
--chain-id ${chain_id} \
--commission-rate ${rate} \
--unlocked=false
```
```Text Unlocked token node theme={null}
./story validator create
--stake ${AMOUNT_TO_STAKE_IN_WEI} \
--moniker ${VALIDATOR_NAME} \
--rpc ${rpc} \
--chain-id ${chain_id} \
--commission-rate ${rate} \
```
This will create the validator corresponding to your validator key saved in `priv_validator_key.json`, providing the validator with `{$AMOUNT_TO_STAKE_IN_WEI}` IP to self-stake.
To participate in consensus, at least 1024 IP must be staked (equivalent to
`1024000000000000000000 wei`)!
Below is a list of optional flags to further customize your validator setup:
**Available Flags:**
* `--stake`: Sets the amount the validator will self-delegate in wei (default is `1024000000000000000000` wei).
* `--moniker`: Defines a custom name for the validator, visible to users on the network.
* `--chain-id`: Specifies the Chain ID for the transaction. By default, this is set to `1516`.
* `--commission-rate`: Sets the validator's commission rate in bips (1% = 100 bips). For instance, `1000` represents a 10% commission (default is `1000`).
* `--explorer`: Specifies the URL of the blockchain explorer (default: [https://www.storyscan.io](https://www.storyscan.io)).
* `--keyfile`: Points to the path of the Tendermint key file (default: `$HOME/.story/story/config/priv_validator_key.json`).
* `--max-commission-change-rate`: Sets the maximum rate at which the validator's commission can change, in bips. For example, `100` represents a maximum change of 1% (default is `1000`).
* `--max-commission-rate`: Defines the maximum commission rate the validator can charge, in bips. For instance, `5000` allows a 50% maximum rate (default is `5000`).
* `--rpc`: Sets the RPC URL to connect to the network (default: [https://mainnet.storyrpc.io](https://mainnet.storyrpc.io)).
* `--unlocked`: Determines if unlocked token staking is supported (`true` for unlocked staking, `false` for locked staking). By default, this is set to `true`.
* `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value
### Example creation command use
```bash theme={null}
story validator create
--stake 1024000000000000000000
--moniker "timtimtim"
--commission-rate 700
--validator-pubkey "" # if you dont have a .env
--rpc "https://mainnet.storyrpc.io"
--chain-id 1514
```
### Verifying your validator
Once created, please use the `Explorer URL` to confirm the transaction. If successful, you should see your validator pub key (*found in your`priv_validator_key.json` file)* listed as part of the following endpoint:
```bash theme={null}
curl https://testnet.storyrpc.io/validators | jq .
```
Congratulations, you are now one of Story’s very first IP validators!
## Validator Staking
To stake to an existing validator, run the following command:
```bash theme={null}
./story validator stake \
--validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \
--stake ${AMOUNT_TO_STAKE_IN_WEI}
--staking-period ${STAKING_PERIOD}
```
* Note that your own `${VALIDATOR_PUB_KEY_IN_HEX}`may be found by running the `./story validator export` command as the `Compressed Public Key (hex)`.
* You must stake at least 1024 IP worth (`*1024000000000000000000 wei`) for the transaction to be valid
Once staked, you may use the `Explorer URL` to confirm the transaction. As mentioned earlier, you may use our [validator endpoint](https://mainnet.storyrpc.io/validators) to confirm the new voting power of the validator.
**Available Flags:**
* `--validator-pubkey`: (string) The public key of the validator to stake to
* `--stake`: (string) The amount of IP to stake in wei
* `--chain-id`: (int) Chain ID to use for the transaction (default: 1514)
* `--explorer`: (string) URL of the blockchain explorer
* `--help`, `-h`: Display help information for stake command
* `--rpc`: (string) RPC URL to connect to the network
* `--staking-period`: (stakingPeriod) Staking period (options: "flexible", "short", "medium", "long") (default: flexible)
* `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value
### Example staking command use
```bash theme={null}
./story validator stake \
--validator-pubkey 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984 \
--stake 1024000000000000000000
--staking-period "short"
```
## Validator Unstaking
To unstake from a validator, run the following command:
```bash theme={null}
./story validator unstake \
--validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \
--unstake ${AMOUNT_TO_UNSTAKE_IN_WEI} \
--delegation-id ${ID_STAKING_PERIOD}
```
This will unstake `${AMOUNT_TO_UNSTAKE_IN_WEI}` IP from the selected validator. You must unstake at least 1024 IP worth (`*1024000000000000000000 wei`) for the transaction to be valid.
Like in the staking operation, please use the `Explorer URL` to confirm the transaction and our [validator endpoint](https://mainnet.storyrpc.io/validators) to double-check the newly reduced voting power of the validator.
**Available Flags:**
* `--chain-id`: (int) Chain ID to use for the transaction (default: 1514)
* `--delegation-id`: (uint32) The delegation ID (0 for flexible staking)
* `--explorer`: (string) URL of the blockchain explorer (default: "[https://www.storyscan.io](https://www.storyscan.io)")
* `--help`, `-h`: Help for unstake command
* `--rpc`: (string) RPC URL to connect to the network (default: "[https://mainnet.storyrpc.io](https://mainnet.storyrpc.io)")
* `--unstake`: (string) Amount to unstake in wei
* `--validator-pubkey`: (string) Validator's hex-encoded compressed 33-byte secp256k1 public key
* `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value
### Example unstaking command use
```bash theme={null}
./story validator unstake \
--validator-pubkey 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984 \
--unstake 1024000000000000000000 \
--delegation-id 1
```
## Validator Stake-on-behalf
To stake on behalf of another delegator, run the following command:
```bash theme={null}
./story validator stake-on-behalf \
--delegator-address ${DELEGATOR_EVM} \
--validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \
--stake ${AMOUNT_TO_STAKE_IN_WEI} \
--staking-period ${STAKING_PERIOD} \
--rpc
--chain-id
```
This will stake `${AMOUNT_TO_STAKE_IN_WEI}` IP to the validator on behalf of the provided delegator. You must stake at least 1024 IP worth (`*1024000000000000000000 wei`) for the transaction to be valid.
Like in the other staking operations, please use the `Explorer URL` to confirm the transaction and our [validator endpoint](https://mainnet.storyrpc.io/validators) to double-check the increased voting power of the validator.
**Available Flags:**
* `--chain-id`: (int) Chain ID to use for the transaction (default: 1514)
* `--delegator-address`: (string) Delegator's EVM address
* `--explorer`: (string) URL of the blockchain explorer (default: "[https://www.storyscan.io](https://www.storyscan.io)")
* `--help`, `-h`: Help for stake-on-behalf command
* `--rpc`: (string) RPC URL to connect to the network (default: "[https://mainnet.storyrpc.io](https://mainnet.storyrpc.io)")
* `--stake`: (string) Amount for the validator to self-delegate in wei
* `--staking-period`: (stakingPeriod) Staking period (options: "flexible", "short", "medium", "long") (default: flexible)
* `--validator-pubkey`: (string) Validator's hex-encoded compressed 33-byte secp256k1 public key
* `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value
### Example Stake-on-behalf command use
```bash theme={null}
./story validator stake-on-behalf \
--delegator-address 0xF84ce113FCEe12d78Eb41590c273498157c91520 \
--validator-pubkey 03e42b4d778cda2f3612c85161ba7c0aad1550a872f3279d99e028a1dfa7854930 \
--stake 1024000000000000000000 \
--staking-period "short" \
--rpc \
--chain-id
```
## Validator Unstake-on-behalf
You may also unstake on behalf of delegators. However, to do so, you must be registered as an authorized operator for that delegator. To unstake on behalf of another delegator as an operator, run the following command:
```bash theme={null}
./story validator unstake-on-behalf \
--delegator-address ${DELEGATOR_PUB_KEY_IN_HEX} \
--validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \
--unstake ${AMOUNT_TO_STAKE_IN_WEI} \
--rpc \
--chain-id
```
This will unstake `${AMOUNT_TO_STAKE_IN_WEI}` IP from the validator on behalf of the delegator, assuming you are a registered operator for that delegator. You must unstake at least 1024 IP worth (`*1024000000000000000000 wei`) for the transaction to be valid.
Like in the other staking operations, please use the `Explorer URL` to confirm the transaction and our [validator endpoint](https://mainnet.storyrpc.io/validators) to double-check the decreased voting power of the validator.
**Available Flags:**
* `--chain-id`: (int) Chain ID to use for the transaction (default: 1514)
* `--delegator-address`: (string) Delegator's EVM address
* `--explorer`: (string) URL of the blockchain explorer (default: "[https://www.storyscan.io](https://www.storyscan.io)")
* `--help`, `-h`: Help for unstake-on-behalf command
* `--rpc`: (string) RPC URL to connect to the network (default: "[https://mainnet.storyrpc.io](https://mainnet.storyrpc.io)")
* `--unstake`: (string) Amount to unstake in wei
* `--validator-pubkey`: (string) Validator's hex-encoded compressed 33-byte secp256k1 public key
* `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value
### Example Unstake-on-behalf command use
```bash theme={null}
./story validator unstake-on-behalf \
--delegator-address 0xF84ce113FCEe12d78Eb41590c273498157c91520 \
--validator-pubkey 03e42b4d778cda2f3612c85161ba7c0aad1550a872f3279d99e028a1dfa7854930 \
--unstake 1024000000000000000000 \
--rpc \
--chain-id
```
## Validator Unjail
In case a validator becomes jailed, for example if it experiences substantial downtime, you may use the following command to unjail the targeted validator:
```Text Bash theme={null}
./story validator unjail \
--rpc
--chain-id
```
Note that you will need at least 1 IP in the wallet submitting the transaction for the transaction to be valid.
**Available Flags:**
* `--chain-id`: (int) Chain ID to use for the transaction
* `--explorer`: (string) URL of the blockchain explorer
* `--rpc`: (string) RPC URL to connect to the network
* `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value
### Example unjail command use
```bash theme={null}
./story validator unjail \
--rpc \
--chain-id
```
## Validator Unjail-on-behalf
If you are an authorized operator, you may unjail a validator on their behalf using the following command:
```bash theme={null}
./story validator unjail-on-behalf \
--validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \
--rpc \
--chain-id
```
**Available Flags:**
* `--chain-id`: (int) Chain ID to use for the transaction
* `--explorer`: (string) URL of the blockchain explorer
* `--rpc`: (string) RPC URL to connect to the network
* `--validator-pubkey`: (string) Validator's hex-encoded compressed 33-byte secp256k1 public key
* `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value
### Example unjail-on-behalf command use
```bash theme={null}
./story validator unjail-on-behalf \
--rpc \
--chain-id
```
## Validator rollback
The snapshot must be taken from a node that has already been upgraded. That
means the snapshot node has to do the rewind.
Recovery Options for Nodes Stuck in Long Rewind After Upgrade:
Wait for resync: Let the node catch up naturally. This may take time depending on the rewind depth and network bandwidth.
Apply a snapshot: Use a snapshot from a node that has already been upgraded to Pectra. You can generate your own or obtain one from trusted community providers.
```bash theme={null}
./story validator rollback \
--rpc \
--chain-id
```
\*\* Avaliable Flages:\*\*
* `--api-address`: (string) The API server address to listen on (default "\***\*\*\*\***:1317")
* `--api-enable`: (bool) Define if the API server should be enabled
* `--api-enable-unsafe-cors`: (bool) Enable unsafe CORS for API server
* `--api-idle-timeout`: (uint) Define the API server idle timeout (in seconds) (default 10)
* `--api-max-header-bytes`: (uint) Define the API server max header (in bytes) (default 8192)
* `--api-read-header-timeout`: (uint) Define the API server read header timeout (in seconds) (default 10)
* `--api-read-timeout`: (uint) Define the API server read timeout (in seconds) (default 10)
* `--api-write-timeout`: (uint) Define the API server write timeout (in seconds) (default 10)
* `--app-db-backend`: (string) The type of database for application and snapshots databases (default "goleveldb")
* `--engine-endpoint`: (string) An EVM execution client Engine API http endpoint (default "[http://localhost:8551](http://localhost:8551)")
* `--engine-jwt-file`: (string) The path to the Engine API JWT file
* `--evm-build-delay`: (duration) Minimum delay between triggering and fetching a EVM payload build (default 600ms)
* `--evm-build-optimistic`: (bool) Enables optimistic building of EVM payloads on previous block finalize (default true)
* `--help`: (bool) help for rollback
* `--home`: (string) The application home directory containing config and data (default "/home/timothyshen/.story/story")
* `--log-color`: (string) Log color (only applicable to console format); auto, force, disable (default "auto")
* `--log-format`: (string) Log format; console, json (default "console")
* `--log-level`: (string) Log level; debug, info, warn, error (default "info")
* `--min-retain-blocks`: (uint) Minimum block height offset during ABCI commit to prune CometBFT blocks
* `--network`: (string) Story network to participate in: story, odyssey, aeneid or local
* `--number`: (uint) number of blocks to rollback (default 1)
* `--pruning`: (string) Pruning strategy (default|nothing|everything) (default "nothing")
* `--snapshot-interval`: (uint) State sync snapshot interval (default 1000)
* `--snapshot-keep-recent`: (uint) State sync snapshot to keep (default 2)
* `--tracing-endpoint`: (string) Tracing OTLP endpoint
* `--tracing-headers`: (string) Tracing OTLP headers
### Example rollback command use
```bash theme={null}
./story validator rollback \
--rpc \
--chain-id
```
## Validator Redelegate
To redelegate from one validator to another, run the following command:
```bash theme={null}
./story validator redelegate \
--validator-src-pubkey ${VALIDATOR_SRC_PUB_KEY_IN_HEX} \
--validator-dst-pubkey ${VALIDATOR_DST_PUB_KEY_IN_HEX} \
--redelegate ${AMOUNT_TO_REDELEGATE_IN_WEI}
--rpc \
--chain-id
```
**Available Flags:**
* `--chain-id`: (int) Chain ID to use for the transaction (default 1514)
* `--delegation-id`: (uint32) The delegation ID (0 for flexible staking)
* `--explorer`: (string) URL of the blockchain explorer (default "[https://www.storyscan.io](https://www.storyscan.io)")
* `--help`, `-h`: Help for redelegate command
* `--redelegate`: (string) Amount to redelegate in wei
* `--rpc`: (string) RPC URL to connect to the network (default "[https://mainnet.storyrpc.io](https://mainnet.storyrpc.io)")
* `--validator-dst-pubkey`: (string) Dst validator's hex-encoded compressed 33-byte secp256k1 public key
* `--validator-src-pubkey`: (string) Src validator's hex-encoded compressed 33-byte secp256k1 public key
* `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value
≈
```bash theme={null}
./story validator redelegate \
--validator-src-pubkey 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984 \
--validator-dst-pubkey 02ed58a9319aba87f60fe08e87bc31658dda6bfd7931686790a2ff803846d4e59c \
--redelegate 1024000000000000000000 \
--rpc \
--chain-id
```
## Validator Redelegate-on-behalf
If you are an authorized operator, you may redelegate from one validator to another on behalf of a delegator using the following command:
```bash theme={null}
./story validator redelegate-on-behalf \
--delegator-address ${DELEGATOR_EVM_ADDRESS} \
--validator-src-pubkey ${VALIDATOR_SRC_PUB_KEY_IN_HEX} \
--validator-dst-pubkey ${VALIDATOR_DST_PUB_KEY_IN_HEX} \
--redelegate ${AMOUNT_TO_REDELEGATE_IN_WEI} \
--rpc \
--chain-id
```
**Available Flags:**
* `--chain-id`: (int) Chain ID to use for the transaction (default 1514)
* `--delegation-id`: (uint32) The delegation ID (0 for flexible staking)
* `--delegator-address`: (string) Delegator's EVM address
* `--explorer`: (string) URL of the blockchain explorer (default "[https://www.storyscan.io](https://www.storyscan.io)")
* `--help`, `-h`: Help for redelegate-on-behalf command
* `--redelegate`: (string) Amount to redelegate in wei
* `--rpc`: (string) RPC URL to connect to the network (default "[https://mainnet.storyrpc.io](https://mainnet.storyrpc.io)")
* `--validator-dst-pubkey`: (string) Dst validator's hex-encoded compressed 33-byte secp256k1 public key
* `--validator-src-pubkey`: (string) Src validator's hex-encoded compressed 33-byte secp256k1 public key
* `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value
### Example redelegate-on-behalf command use
```bash theme={null}
./story validator redelegate-on-behalf \
--delegator-address 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab \
--validator-src-pubkey 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984 \
--validator-dst-pubkey 02ed58a9319aba87f60fe08e87bc31658dda6bfd7931686790a2ff803846d4e59c \
--redelegate 1024000000000000000000 \
--rpc \
--chain-id
```
## Set Operator
Delegators may add operators to unstake or redelegate on their behalf. To add an operator, run the following command:
* `--chain-id` int Chain ID to use for the transaction (default 1514)
* `--explorer` string URL of the blockchain explorer (default "[https://www.storyscan.io](https://www.storyscan.io)")
* `--operator` string Sets an operator to your delegator
* `--rpc` string RPC URL to connect to the network (default "[https://mainnet.storyrpc.io](https://mainnet.storyrpc.io)")
```bash theme={null}
./story validator set-operator \
--operator ${OPERATOR_EVM_ADDRESS} \
--rpc \
--chain-id \
--story-api ${STORY_API_URL}
```
Note that you will need at least 1 IP in the wallet submitting the transaction for the transaction to be valid.
### Example add operator command use
```bash theme={null}
./story validator set-operator \
--operator 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab \
--rpc \
--chain-id \
--story-api http://localhost:1317
```
## Unset Operator
To remove an operator, run the following command:
```bash theme={null}
./story validator unset-operator \
--operator ${OPERATOR_EVM_ADDRESS} \
--rpc \
--chain-id \
--story-api ${STORY_API_URL}
```
### Example Remove Operator command use
```bash theme={null}
./story validator remove-operator \
--operator 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab \
--rpc \
--chain-id \
--story-api http://localhost:1317
```
## Set Rewards Address
To change the address that your delegator receives staking and withdrawal rewards from, you can run the following:
```bash theme={null}
./story validator set-rewards-address \
--rewards-address ${OPERATOR_EVM_ADDRESS} \
--story-api ${STORY_API_URL}
```
Note that you will need at least 1 IP in the wallet submitting the transaction for the transaction to be valid.
### Example Set Withdrawal Address command use
```bash theme={null}
./story validator set-rewards-address \
--rewards-address 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab
--story-api http://localhost:1317
```
## Set Withdrawal Address
To change the address that your delegator receives staking and withdrawal rewards from, you can run the following:
```bash theme={null}
./story validator set-withdrawal-address \
--withdrawal-address ${OPERATOR_EVM_ADDRESS} \
--story-api ${STORY_API_URL}
```
Note that you will need at least 1 IP in the wallet submitting the transaction for the transaction to be valid.
### Example Set Withdrawal Address command use
```bash theme={null}
./story validator set-withdrawal-address \
--withdrawal-address 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab
--story-api http://localhost:1317
```
## Update Validator Commission
To change the commission rate for your validator, you can run the following:
```
./story validator update-validator-commission \
--commission-rate ${NEW_COMMISSION}
```
### Example Update Validator Commission
```
./story validator update-validator-commission \
--commission-rate 5000
```
## Enabling Story API
Prerequisites:
1. Ensure your full node is synced and caught up with latest blocks
Steps to enable:
1. Navigate to `${STORY_DATA_ROOT}/config/story.toml`
2. Set `enable = true` under the `[api]` section
3. Restart the node
Then you could use `http://localhost:1317` as the `-story-api` value
## Migrating a validator to another machine
Before migrating your validator node to a new machine, make sure the current
node is fully shut down. Attempting to restore an active validator could
result in "double signing," a critical error that may lead to the slashing of
your delegated shares.
1. Begin by configuring a new environment for your validator. Ensure that the new full node is fully synced to the latest block on the network.
2. To avoid accidental double-signing, it’s essential to fully shut down the original validator node before activating the new instance. We recommend deleting the Story service file to prevent it from automatically restarting after a system reboot. Additionally, back up both `priv_validator_key.json` and `priv_validator_state.json` and remove it from the current server running the active validator. Skipping these steps could result in missed blocks or other penalties.
```bash theme={null}
# Step 1: Stop the original validator node
sudo systemctl stop .service
# Step 2: Disable the Story service to prevent automatic restarts
sudo systemctl disable .service
# Step 3: Delete the Story service file to prevent it from starting on reboot
sudo rm /etc/systemd/system/.service
# Step 4: Back up the `priv_validator_key.json` file securely, e.g., using SFTP:
# Use an SFTP client or a secure method to download the file without displaying it in the terminal
# If needed for verification purposes only, you may view it with the following command:
cat ~/.story/story/config/priv_validator_key.json
# Step 5: Remove the `priv_validator_key.json` file from the current server
rm ~/.story/story/config/priv_validator_key.json
```
3. Locate `priv_validator_key.json` and `priv_validator_state.json` in the `~/.story/story/config/` directory on your new machine. Replace this file with the backup copy from your old validator.
Before proceeding, shut down the old validator on the original server and do
not restart it!
4. After transferring the private key file, restart the validator node on your new setup. This will reintegrate your validator with the network, enabling it to resume its validation role.
## Private Key Encryption for Validators
This feature allows operators to securely generate, store, and manage private keys in encrypted form through CLI commands—including encrypting keys during setup, migrating unencrypted keys, and safely accessing them for validator operations.
### Overview of Private Key Encryption
Private key encryption is a critical security measure that encrypts sensitive cryptographic keys with a user-defined password. In the context of validator operations, this means that even if a private key file is exposed or accessed by unauthorized parties, it remains unusable without the correct password. Story CLI helps operators enforce stronger security controls while maintaining ease of use by integrating encryption directly into the validator workflow. This feature is designed to mitigate common attack vectors, such as accidental key leaks or unauthorized server access, by preventing private keys from being stored or used in plaintext.
### Initializing a Validator with Private Key Encryption
When setting up a new validator using the Story CLI, operators can generate and encrypt the validator’s private key in one step. By adding the `--encrypt-priv-key` flag during initialization, the CLI will prompt the operator to create a password, which is used to encrypt the generated private key. This encrypted key is then securely stored at `story/config/priv_validator_key.enc`.
Through this enhancement, private keys are never written to disk in plaintext during initialization, significantly reducing the risk of accidental exposure. Once encrypted, the key remains protected, and password decryption is required before any validator-related actions can be performed.
**Example usage:**
```bash theme={null}
./story init --encrypt-priv-key --network local
```
In this example, the validator is initialized on the local network, automatically triggering the private key encryption process. During this step, the operator is prompted to input and confirm a password. The resulting encrypted key file becomes the foundation for all validator operations.
### Using the Encrypted Private Key
Once a private key is encrypted, decryption is required for any key-dependent validator operation. The Story CLI will automatically detect the encrypted key file and prompt for a password when running a validator node. For validator-related CLI commands, however, the path of the encrypted key file should be specified. If no key file path is provided, the CLI will fall back to the private key in the .env file.
The password prompt appears interactively via the CLI, blocking execution if the wrong password is provided. As a result, only authorized operators with the correct password can unlock and use the private key, adding protection against unauthorized access.
This workflow integrates with typical validator commands, preserving the developer experience while strengthening security. For environments that require automation, additional tooling (e.g., password managers or secure key vaults) can be used to manage passwords securely. Still, caution is advised to avoid undermining the purpose of encryption.
**Example:**
```bash theme={null}
./story validator start
# CLI will prompt: "Enter password to decrypt private key:"
```
This behavior applies consistently across all validator commands requiring private key access.
### Encrypting an Existing Private Key
For operators already running validators with unencrypted private keys, Story CLI now provides a simple migration path to adopt encrypted key storage. Using the encrypt command, you can securely encrypt the existing private key (typically defined in the .env file).
Once the command is executed, the CLI will prompt you to set a password. The private key will be encrypted and stored at a specified path using the --enc-key-file flag. The encrypted key becomes the default for validator operations, allowing the original key in the .env file to be manually removed for better security. This does not apply to validator-related CLI commands, which still require explicit key input.
**Example usage:**
```bash theme={null}
./story encrypt --encrypt-priv-key
```
This enables existing validators to benefit from the enhanced security of encrypted key management without requiring re-initialization. It’s recommended that the password and encrypted key file be securely backed up before deleting the plaintext key from the `.env` file.
### Viewing the Encrypted Private Key
Story CLI introduces a show command to give operators transparency and control over their encrypted keys. This command decrypts the encrypted private key file—specified via the `--enc-key-file` flag—and displays essential information such as the public key and validator address. It mirrors the functionality of the traditional export command but supports encrypted keys.
Operators will be prompted for the encryption password before revealing key details, protecting sensitive information from unauthorized access. For advanced users, the optional `--show-private` flag will reveal the hex-encoded private key. However, this should be used cautiously as it defeats the purpose of encryption and exposes the key to potential compromise.
**Example usage:**
```bash theme={null}
# View public information (e.g., public key, address)
./story key show-encrypted --encrypt-key-file
# View public information and the raw private key (use with caution)
./story key show-encrypted --show-private --enc-key-file
```
It is recommended that `--show-private` be used only in secure environments and for exceptional operational needs, such as recovery or migration to other systems.
# Release Notes
Source: https://docs.story.foundation/network/participate/validators/release-notes
Information on Story execution and consensus client software releases
This page provides information on the story execution and consensus client software release information. You may find execution client releases in [story-geth](https://github.com/piplabs/story-geth/releases) repo, and consensus client releases in [story](https://github.com/piplabs/story/releases) repo.
### Production releases
There are generally four types of releases:
* Major: It requires hardfork upgrade with a predefined upgrade height. Node operators need to upgrade before or on the height. The release will increase minor version number.
* Minor: It doesn't require hardfork upgrade. Node operators are required to upgrade binaries as soon as possible. The release will increase patch version number.
* Fix: It is an urgent fix. Node operators are required to upgrade binaries as soon as possible. The release will increase minor version or patch version number.
* Optional: It is an optional fix. Node operators can upgrade binaries based on needs. The release will increase patch version number.
Each release comes with a release note describing a list of new features or fixes. Released software binaries are also attached in the release note. We currently provide binaries supporting four types of systems: darwin-amd64, darwin-arm64, linux-amd64, and linux-arm64. You may also build your binaries using the commit hash in the release note.
### Release entries
Refer to the following release matrix to run nodes for Mainnet and Aeneid Testnet.
| Network | story-geth | story |
| ------- | ----------------- | ---------------- |
| Mainnet | v1.2.0 (Yasunari) | v1.4.2 (Terence) |
| Aeneid | v1.2.0 (Yasunari) | v1.4.2 (Terence) |
### Terence
[Full release note](https://github.com/piplabs/story/releases/tag/v1.4.2)
* Security fixes
### Polybius
[Full release note](https://github.com/piplabs/story/releases/tag/v1.3.3)
* Backport the fixes for release v1.3.3 (#598)
### Polybius
[Full release note](https://github.com/piplabs/story/releases/tag/v1.3.2)
* Increases the max number of validators from 64 to 80
### Polybius
[Full release note](https://github.com/piplabs/story/releases/tag/v1.3.1)
handling residual rewards by version
### Cosmas
[Full release note](https://github.com/piplabs/story-geth/releases)
Enables Pectra Upgrade for Mainnet
### Cosmas (Testnet Only)
[Full release note](https://github.com/piplabs/story-geth/releases)
* EIP-7702 – Set EOA account code
* EIP-2537 – BLS12-381 curve operations
* EIP-7623 – Increase calldata cost
* EIP-7685 – Execution layer requests (EIP-7685)
### Ovid
[Full release note](https://github.com/piplabs/story/releases/tag/v1.2.0)
* (app) change MaxBytes of block in consensus params (#529)
* (x/evmengine) support snap sync for execution engine (#506)
* (cli) add encryption for validator private key (#494)
* (cli) add with-comet flag (#518)
* (api) add withdrawal queue query (#496)
* (app) disallow unexported fields in cosmos tx (#529)
* (x/evmengine) add validation for max size of tx (#529)
* (cli) fix validator not found error during validator creation (#515)
* (cli) add validation for max commission change rate (#489)
* (cli) add self-delegation validation to unjail command (#510)
* (api) fix incorrect type conversion between integer types (#492)
### Ovid
[Full release note](https://github.com/piplabs/story-geth/releases/tag/v1.0.2)
* \[ipgraph] prevent overflow in calculating gas of add parent (#102)
* \[ipgraph] apply acl to function hasAncestorIp (#106)
# Troubleshooting
Source: https://docs.story.foundation/network/participate/validators/troubleshooting
Common problems and solutions when running Story nodes
Welcome to Story node troubleshooting! This section covers common problems and solutions when running Story nodes.
### Node Setup
See the [system specs](/network/operating-a-node/node-setup-mainnet)
\~700
Yes, it's EVM-compatible. Story's execution client is a fork of Geth with our custom precompiles, which enhance the IP graph's performance while maintaining strict EVM compatibility. Other Ethereum execution clients, such as RETH and Erigon, can be supported later.
Our consensus mechanism is CometBFT
Batch RPCs are supported - for Geth there is a 1K limit and on the consensus side there is 10 request limit
Yes, WS is enabled on the execution client, and is recommended for subscription use-cases. It is open on port 8546
Please see Geth's latest JSON-RPC documentation for a full comprehensive list [here](https://ethereum.org/en/developers/docs/apis/json-rpc/#web3_clientversion). In the future, we may add more.
We recommend employing standard in-memory caching with a 1-10 min TTL based on the RPC method
Use `eth_syncing` RPC call on the execution client to check if the node is sync and `eth_blockNumber` for getting the latest block
`eth_call` / `eth_getLogs` / `eth_getBlockByNumber` \
We are still running latency tests to get a sense of response times.
No, not at the moment.
Not yet, but we are working on it.
### Common Issues
**Error:**
```bash theme={null}
ERRO !! Fatal error occurred, app died️ unexpectedly !! err="create db: failed to initialize database:
```
**Solution:**
1. Save your validator state:
```bash theme={null}
cp $HOME/.story/story/data/priv_validator_state.json $HOME/.story/story/priv_validator_state.json.backup
```
> 🚧 Be very careful with this file, especially if your validator is already signing blocks.
* Check your the database backend type, your node must support the same as you are using the snapshot:
```bash theme={null}
cat $HOME/.story/story/config/story.toml
```
Default is `app-db-backend = "goleveldb"`. The fallback is the `db_backend` value set in CometBFT's `config.toml`.
```bash theme={null}
cat $HOME/.story/story/config/config.toml
```
**Problem:** Need to adjust gas fees on RPC node
**Solution:**
Add the `--rpc.txfee` flag to your geth startup command:
```bash theme={null}
sudo tee /etc/systemd/system/story-geth.service > /dev/null <
**Error:**
```bash theme={null}
ERRO Failed to send PacketPing module=p2p peer=19fa6dd52e72e4e85bbb873b705282cf73217a6b@158.220.80.96:40128 err="write tcp 139.59.139.135:26656->158.220.80.96:40128: write: broken pipe"
```
Solution:
* If the node is synchronized, you can ignore this error. Your client may be a little behind.
* If the node stops, you should restart the services.
An error occurs when starting the cosmovisor:
```bash theme={null}
panic: failed to read upgrade info from disk unexpected end of JSON input
```
Solution:
* You must ensure that the installed cosmovisor version must be at least [v1.7.0.](https://docs.cosmos.network/main/build/tooling/cosmovisor)
* Then check your info file (edit version `v0.13.0` in your case):
```bash theme={null}
cat $HOME/.story/story/cosmovisor/upgrades/v0.13.0/upgrade-info.json
```
If you don\`t have create new one:
```bash theme={null}
echo '{"name":"v0.13.0","time":"0001-01-01T00:00:00Z","height":858000}' > $HOME/.story/story/cosmovisor/upgrades/v0.13.0/upgrade-info.json
```
Find out more about automatic updates with cosmovisor [here](/network/operating-a-node/node-setup-mainnet#custom-automation).
Error:
```bash theme={null}
INFO HTTP server stopped
INFO IPC endpoint closed
```
Solution:
* It looks like port 8551 stopping, the background process running `iptables` blocking ip and port and access posix.
* For solution try uninstall `ufw posix` and `iptables`:
```bash theme={null}
iptables -I INPUT -s localhost -j ACCEPT
```
Error:
```bash theme={null}
panic: Faile to consensus state: found signature from the same key
```
Solution:
* The validator has been double signed. It is currently not possible to restore the validator after it has been double signed.
* To avoid such situations, see this post on how to correctly [migrate a validator to another machine](/network/become-a-validator#migrating-a-validator-to-another-machine).
Error:
```bash theme={null}
4-11-26 08:42:20.302 ERRO !! Fatal error occurred, app died️ unexpectedly !! err="failed to validate create flags: missing required flag(s): moniker" stacktrace="[errors.go:39 flags.go:173 validator.go:168 validator.go:384 command.go:985 command.go:1117 command.go:1041 command.go:1034 cmd.go:34 main.go:10 proc.go:271 asm_amd64.s:1695]"
```
Solution:
* You missed flag `--moniker`.
* The command to create a new validator should look like this:
```bash theme={null}
./story validator create --stake ${AMOUNT_TO_STAKE_IN_WEI} --moniker ${VALIDATOR_NAME}
```
See more options [here](/network/become-a-validator#validator-creation).
Error:
```bash theme={null}
ERRO failed to process message msg_type= *consensus.VoteMessage err:" error adding vote"
```
Solution:
* It looks like your node is down. To get started, check the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet).
* If you have up-to-date binary - try updating peers, this usually happens when a node loses p2p communication:
```bash theme={null}
PEERS="..."
sed -i -e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*persistent_peers *=.*/persistent_peers = \"$PEERS\"/}" $HOME/.story/story/config/config.toml
```
Error:
```bash theme={null}
ERRO failed signing vote module=consensus height=403750 round=0 vote="Vote{23:B12C6AE31E8E 403750/00/SIGNED_MSG_TYPE_PREVOTE(Prevote) FA591EB1E540 000000000000 000000000000 @ 2024-11-08T16:58:10.375918193Z}" err="error signing vote: height regression. Got 403750, last height 420344"
```
Solution:
* Looks like you have a problem with your `priv_validator_state` of validator.
> 🚧 Be very careful with this file, especially if your validator is already signing blocks.
* You can make a copy of your state with a command:
```bash theme={null}
cp $HOME/.story/story/data/priv_validator_state.json $HOME/.story/story/priv_validator_state.json.backup
```
Check your validator state:
```bash theme={null}
cat $HOME/.story/story/data/priv_validator_state.json
```
* If you get this error, you can reset your state (🚧 ONLY IF YOUR VALIDATOR HAS NOT YET SIGNET BLOCKS).
* Stop node.
```bash theme={null}
sudo tee $HOME/.story/story/data/priv_validator_state.json > /dev/null <
Error:
```bash theme={null}
ERRO !! Fatal error occurred, app died️ unexpectedly !! err="unknown flag: --home"
```
Solution:
* It looks like a misconfiguration. You must try to remove the `--home` flag from the startup command.
* Your systemd to run might look like this:
Error:
```bash theme={null}
Fatal: Failed to register the Ethereum service: incompatible state scheme, stored: path, provided: hash
```
Solution:
* You have problems with the state of validator or a corrupted database.
* Try using a snapshot.
> 🚧 Be very careful with this file, especially if your validator is already signing blocks.
* We have described how to reset your state [here](/network/more/troubleshooting#error-signing-vote).
## Failed to reconnect to peer
Error:
```bash theme={null}
24-09-25 06:38:45.235 ERRO Failed to reconnect to peer. Beginning exponential backoff module=p2p addr=e0600fa5f2129e647ef30a942aac1695201ff135@65.109.115.98:26656 elapsed=2m29.598884906s
```
Solution:
* If the node is synchronized and not far behind, you can ignore this error.
* If the node is lagging or has stopped completely, try updating peers, this usually happens when a node loses p2p communication:
```bash theme={null}
PEERS="..."
sed -i -e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*persistent_peers =./persistent_peers = \"$PEERS\"/}" $HOME/.story/story/config/config.toml
```
Warn:
```bash theme={null}
WARN Processing finalized payload halted while evm syncing (will retry) payload_height=...
```
Solution:
* It just means that story-geth is syncing, you can ignore this warn.
* However, if it takes a long time, we recommend that you stop the processes one at a time and start them again later in the following order:
```bash theme={null}
sudo systemctl stop story-geth story
sudo systemctl daemon-reload
sudo systemctl start story-geth
sudo systemctl enable story-geth
sudo systemctl daemon-reload
sudo systemctl start story
sudo systemctl enable story
```
Error:
```bash theme={null}
ERRO error in proxyAppConn.FinalizeBlock module=consensus err="module manager preblocker: wrong app version 0, upgrade handler is missing for upgrade plan"
```
Solution:
* Looks like you missed an update.
* To get started, check the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet).
Error:
```bash theme={null}
ERRO !! Fatal error occurred, app died️ unexpectedly !! err="home directory contains unexpected file(s), use --force to initialize anyway"
```
Solution:
* This means that you have already initialized the node.
* `$HOME/.story/story` directory created, and there are files in it. Delete it, or try with it.
Error:
```bash theme={null}
ERRO !! Fatal error occurred, app died️ unexpectedly ! err="create comet node: create node
```
Solution:
* It appears that your node is using incorrect versions.
* Check the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet).
* And most likely you need to perform a rollback binary to current versions.
Error:
```bash theme={null}
ERRO catchup replay: WAL does not contain
```
Solution:
* Looks like an `AppHash` issue.
* To get started, upgrade to the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet).
* If your versions are newer than the current ones, perform a rollback.
Error:
```bash theme={null}
ERRO !! Fatal error occurred, app died️ unexpectedly !! err="load engine JWT file: read jwt file: open /root/.story/geth/odyssey/geth/jwtsecret: no such file or directory
```
Solution:
* It seems your node can't get `jwtsecret`.
* Check your `WorkingDirectory` in your `geth-service` , by default `WorkingDirectory=$HOME/.story/geth`.
* Check all paths, you can get your `jwtsecret`with command (for odyssey network):
```bash theme={null}
cat .story/geth/odyssey/geth/jwtsecret
```
Error:
```bash theme={null}
ERRO Couldn't connect to any seeds module=p2p
```
Solution:
* If the node is synchronized and not far behind, you can ignore this error.
* If the node is lagging or has stopped completely, try updating seeds/peers, it usually happens when a node loses p2p communication (we recommend that you stop the node and delete the addrbook).
```bash theme={null}
rm -rf $HOME/.story/story/config/addrbook.json
SEEDS="..."
PEERS="..."
sed -i -e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*seeds *=.*/seeds = \"$SEEDS\"/}" \
-e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*persistent_peers *=.*/persistent_peers = \"$PEERS\"/}" $HOME/.story/story/config/config.toml
```
Warn:
```bash theme={null}
WRN Processing finalized payload; evm syncing
WRN Processing finalized payload failed: evm fork choice update (will retry) status="" err="rpc forkchoice updated v3: beacon syncer reorging"
```
Solution:
* Everything is fine, it just means that `story-geth` is syncing, which takes some time.
* If the node is not far behind, you can ignore this warning.
## Dial tcp 127.0.0.1:9090
Warn:
```bash theme={null}
WRN error getting latest block error:"rpc error: dial tcp 127.0.0.1:9090"
```
Solution:
* The logs show a connection failure on port `9090`.
* Check the listening ports:
```bash theme={null}
sudo ss -tulpn | grep LISTEN
```
* If other node uses `9090`, then modify it to another.
* Normally, this WARNING should not affect the performance of your node.
Error:
```bash theme={null}
ERRO Error in validation module=blocksync err="wrong Block[dot]Header[dot]AppHash Expected [...]
```
Solution:
* `Wrong AppHash` type logs means the story node version you are using is wrong.
* Upgrade to the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet).
* If your versions are newer than the current ones, perform a rollback.
Error:
```bash theme={null}
ERRO Connection failed @ sendRoutine module=p2p peer=...
ERRO Stopping peer for error module=p2p peer=...
```
Solution:
* If the node is synchronized and not far behind, you can ignore this error.
* If the node is lagging or has stopped completely, try updating peers, this usually happens when a node loses p2p communication:
```bash theme={null}
PEERS="..."
sed -i -e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*persistent_peers =./persistent_peers = \"$PEERS\"/}" $HOME/.story/story/config/config.toml
```
Error:
```bash theme={null}
ERRO !! Fatal error occurred, app died️ unexpectedly ! err="create comet node: create node: info.Moniker must be valid non-empty
```
Solution:
* Looks like a problem with your node moniker.
* Be sure to use `""` when executing init:
```bash theme={null}
story init --network "..." --moniker "..."
```
* Go to config, find the moniker and put it inside `""` only:
```bash theme={null}
sudo nano ~/.story/story/config/config.toml
```
Error:
```bash theme={null}
Fatal error occurred, app died️ unexpectedly ! err="create comet node: create node: invalid address (26656):
```
Solution:
* The logs report a connection failure on port `26656`.
* Check the listening ports:
```bash theme={null}
sudo ss -tulpn | grep LISTEN
```
* If another node is using `26656`, change it to another and keep the default `26656` for story in the `P2P configuration` options in `config`:
```bash theme={null}
sudo nano ~/.story/story/config/config.toml
```
Warn:
```bash theme={null}
WARN Beacon client online, but no consensus updates received in a while. Please fix your beacon client to follow the chain!
Served eth_coinbase eth_coinbase does not exist
```
Solution:
* This error indicates that the network has stopped.
Warn:
```bash theme={null}
WARN Verifying proposal failed: push new payload to evm (will retry) status="" err="new payload: rpc new payload v3: Post \"http://localhost:8551\": round trip: dial tcp 127.0.0.1:8551: connect: connection refused" stacktrace="[errors.go:39 jwt.go:41 client.go:259 client.go:180 client.go:724 client.go:590 http.go:229 http.go:173 client.go:351 engineclient.go:101 msg_server.go:183 proposal_server.go:34 helpers.go:30 proposal_server.go:33 tx.pb.go:299 msg_service_router.go:175 tx.pb.go:301 msg_service_router.go:198 prouter.go:74 abci.go:520 cmt_abci.go:40 abci.go:85 local_client.go:164 app_conn.go:89 execution.go:166 state.go:1381 state.go:1338 state.go:2055 state.go:910 state.go:836 asm_amd64.s:1695]"
WARN Verifying proposal
```
Solution:
* It looks like port 8551 stopping, the background process running `iptables` blocking ip and port and access posix.
* For solution try uninstall `ufw posix` and `iptables`:
```bash theme={null}
iptables -I INPUT -s localhost -j ACCEPT
```
# Disclaimers
Source: https://docs.story.foundation/notices
# Quickstart
Source: https://docs.story.foundation/quickstart
Start building on Story quickly.
You want to start building on Story quickly... so let's get started!
***
## Add Network
Enable Story's mainnet or testnet for your wallet.
Connect your wallet to Story's mainnet.
Connect your wallet to Story's 'Aeneid' testnet.
## Skip everything. Go to the code.
This is a clone-able quickstart for you to check out. You can clone it
directly and follow the associated README.
This is a clone-able quickstart for you to check out. You can clone it
directly and follow the associated README.
This is a boilerplate for you to check out. You can clone it directly, study
the example smart contracts, and follow the associated README for running
the tests.
## Story Network Infra
See [Network Info](/network/network-info/overview) for all RPC, explorer, and faucet info.
## Use our SDKs
Check out the entire [SDK Reference](/sdk-reference) to see an explanation + example for every function in our 🛠️ **TypeScript SDK** (can use this in React as well) and 🐍 **Python SDK**.
We have also built a [🛠️ TypeScript SDK Guide](/developers/typescript-sdk), which is more of a step-by-step walkthrough, with its own in-depth tutorials for popular functions and use cases.
## Deployed Smart Contracts
Check out the addresses for the deployed smart contracts [here](/developers/deployed-smart-contracts). Note that there are two different kinds of contracts:
* [Story Protocol Core](https://github.com/storyprotocol/protocol-core-v1) - This repository contains the core protocol logic, consisting of a thin IP registry (the [IP Asset Registry](/concepts/registry/ip-asset-registry)), a set of [🧱 Modules](/concepts/modules) defining logic around [📜 Licensing](/concepts/licensing-module), [💸 Royalty](/concepts/royalty-module), [❌ Dispute](/concepts/dispute-module), metadata, and a module manager for administering module and user access control.
* [Story Protocol Periphery](https://github.com/storyprotocol/protocol-periphery-v1)- Whereas the core contracts deal with the underlying protocol logic, the periphery contracts deal with protocol extensions that greatly increase UX and simplify IPA management. This is mostly handled through the [📦 SPG](/concepts/spg).
## Use our API
Check out the entire [API Reference](/api-reference) for learning how to use our API. For common things like fetching gas price, average block time, market cap, token price, and more, check out the [Blockscout API](/api-reference/blockscout-api).
## Register IP on Story
Let's start with the most basic question: *"What does it take to register IP on Story in my app? How do I do this?"*
To register IP on Story, you'll first need an NFT. If your IP is an ERC-721 NFT (ex. an Azuki or Pudgy Penguin on Story), you're already set. If not, you must mint an NFT to represent your off-chain IP. And don't worry, we'll help you do this in the following tutorials.
Next you'd register that NFT on Story, ultimately creating an [🧩 IP Asset](/concepts/ip-asset). An "IP Asset" is your IP registered on Story, empowered by:
* all of Story's [🧱 Modules](/concepts/modules) like transparent licensing, automatic royalty payments, and disputing of wrongfully registered IP
* IP protection through the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license)
Follow the below tutorials to register IP on Story:
Learn how to register IP on Story using the TypeScript SDK.
Learn how to register IP on Story using the Smart Contracts.
### Difference Between IP Metadata vs. NFT Metadata
A common question we get from developers while registering their IP on Story is: *"What metadata should be/is expected to be attached to the NFT, and then separately, the IP Asset?"*
To answer that question, please see [NFT vs. IP Metadata](/concepts/ip-asset/overview#nft-vs-ip-metadata).
## Licensing Your IP
You may be wondering, *"How do I take advantage of Story's on-chain licensing? How do I make sure my registered IP has a license ready to go?"*
Before you attach any sort of licenses or license terms to your [🧩 IP Asset](/concepts/ip-asset), it would be best to first understand what the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license) actually is. This "PIL" is what defines the available [License Terms](/concepts/licensing-module/license-terms) on Story, which in turn - when attached to an IP Asset - is what defines how others can use (commercially, create derivatives, etc) that IP Asset.
Our tutorials will show you exactly how to attach license terms to your IP Asset:
Learn how to attach license terms to your IP on Story using the TypeScript
SDK.
Learn how to attach license terms to your IP on Story using the Smart
Contracts.
For more information on licensing and the terminology behind it, check out the [📜 Licensing Module](/concepts/licensing-module).
## Royalties / Revenue Sharing
Now you may be wondering, *"How do I set up automatic royalty sharing between my IP Asset and someone else's? How do I then collect that payment?"*
When you attach [License Terms](/concepts/licensing-module/license-terms) to your [🧩 IP Asset](/concepts/ip-asset), you can specify certain commercial terms such as `commercialRevShare`, which is the amount of revenue (from any source, original & derivative) that must be shared by derivative works with the original IP. See the above section for licensing questions.
If someone then creates a derivative of my IP Asset - which has a `commercialRevShare` of let's say 10% in its license terms - and earns revenue on it, Story enforces the share of this revenue through the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license) (otherwise resulting in an on-chain dispute using the [❌ Dispute Module](/concepts/dispute-module) or traditional legal arbitration) and then handles the upstream revenue share at the protocol level. If the derivative work earns 100 \$WIP, my original IP Asset could claim 10 \$WIP.
Our tutorials will show you exactly how to claim revenue:
Learn how to claim revenue on Story using the TypeScript SDK.
Learn how to claim revenue on Story using the Smart Contracts.
For more information on royalty and how it functions, check out the [Royalty Module](/concepts/royalty-module).
## Disputing
Now you may be wondering, *"How can I actually dispute someone else's IP if they steal mine, or don't pay me proper revenue for using it?"*
There are two main philosophies/ways to take down "bad" IP.
The first is the [🕵️ Story Attestation Service](/concepts/story-attestation-service). This compromises of a bunch of infringement detection providers that, upon IP registration, automatically review the IP - using their own methods, whether it's AI, manual checking, etc - and flag it if the IP is infringing (ex. registering a picture of Pikachu). Then, any IP discovery platform like the [IP Portal](https://portal.story.foundation) can surface the reviews and let users decide if they want to use an IP or not.
*For example, an IP that has hundreds of flags from different infringement providers probably isn't a legitimate IP.*
The second is using the [❌ Dispute Module](/concepts/dispute-module) to officially tag & block IP at the protocol level. Anyone can flag an IP and it will be sent off to arbitration partners like [UMA](https://uma.xyz) who will decide its fate. If officially tagged, an IP can no longer earn revenue or create derivatives via the protocol.
*For example, if someone doesn't make a proper payment for using an IP commercially, or uses it in a disallowed territory, or contains NSFW content.*
Our tutorials will show you exactly how to raise a dispute on-chain:
Learn how to dispute IP on Story using the TypeScript SDK.
For more information on filing a dispute on-chain, check out the [❌ Dispute Module](/concepts/dispute-module).
# Blockscout API
Source: https://docs.story.foundation/api-reference/blockscout-api
Get gas price, average block time, market cap, token price, and more.
Storyscan has a public API endpoint that returns gas price, average block time, market cap, token price (coin gecko), and several other stats: `https://www.storyscan.io/api/v2/stats`
Here is an example response ⤵️
```json theme={null}
{
"average_block_time": 2364,
"coin_image": "https://coin-images.coingecko.com/coins/images/54035/small/Transparent_bg.png?1738075331",
"coin_price": "4.83",
"coin_price_change_percentage": null,
"gas_price_updated_at": "2025-03-10T14:47:27.175157Z",
"gas_prices": {
"slow": 0.1,
"average": 0.57,
"fast": 1.05
},
"gas_prices_update_in": 11735,
"gas_used_today": "147032238744",
"market_cap": "1209228486.984",
"network_utilization_percentage": 10.8968948333333,
"secondary_coin_image": null,
"secondary_coin_price": null,
"static_gas_price": null,
"total_addresses": "686024",
"total_blocks": "1765700",
"total_gas_used": "0",
"total_transactions": "5606580",
"transactions_today": "221320",
"tvl": null
}
```
# Introduction
Source: https://docs.story.foundation/api-reference/consensus-client/introduction
Example section for showcasing API endpoints
In order to use the Consensus Client API, you must run your own node. See the [Node Setup Guide](/network/operating-a-node/node-setup-mainnet).
We have included the API Reference here so you know what to expect in the response.
# GetAuthParams
Source: https://docs.story.foundation/api-reference/cosmos-originauth/getauthparams
/api-reference/consensus-client/consensus-client-api.json get /auth/params
# GetDelegationByValidatorAddressDelegatorAddress
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getdelegationbyvalidatoraddressdelegatoraddress
/api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/delegations/{delegator_addr}
# GetDelegationsByDelegatorAddress
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getdelegationsbydelegatoraddress
/api-reference/consensus-client/consensus-client-api.json get /staking/delegations/{delegator_addr}
# GetDelegatorUnbondingDelegation
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getdelegatorunbondingdelegation
/api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/delegations/{delegator_addr}/unbonding_delegation
# GetHistoricalInfoByHeight
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/gethistoricalinfobyheight
/api-reference/consensus-client/consensus-client-api.json get /staking/historical_info/{height}
# GetRedelegationsByDelegatorAddress
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getredelegationsbydelegatoraddress
/api-reference/consensus-client/consensus-client-api.json get /staking/delegators/{delegator_addr}/redelegations
# GetStakingParams
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getstakingparams
/api-reference/consensus-client/consensus-client-api.json get /staking/params
# GetStakingPool
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getstakingpool
/api-reference/consensus-client/consensus-client-api.json get /staking/pool
# GetUnbondingDelegationsByDelegatorAddress
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getunbondingdelegationsbydelegatoraddress
/api-reference/consensus-client/consensus-client-api.json get /staking/delegators/{delegator_addr}/unbonding_delegations
# GetValidatorByValidatorAddress
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getvalidatorbyvalidatoraddress
/api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}
# GetValidatorDelegationsByValidatorAddress
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getvalidatordelegationsbyvalidatoraddress
/api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/delegations
# GetValidators
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getvalidators
/api-reference/consensus-client/consensus-client-api.json get /staking/validators
# GetValidatorsByDelegatorAddress
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getvalidatorsbydelegatoraddress
/api-reference/consensus-client/consensus-client-api.json get /staking/delegators/{delegator_addr}/validators
# GetValidatorsByDelegatorAddressValidatorAddress
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getvalidatorsbydelegatoraddressvalidatoraddress
/api-reference/consensus-client/consensus-client-api.json get /staking/delegators/{delegator_addr}/validators/{validator_addr}
# GetValidatorUnbondingDelegations
Source: https://docs.story.foundation/api-reference/cosmos-originstaking/getvalidatorunbondingdelegations
/api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/unbonding_delegations
# Get a Dispute
Source: https://docs.story.foundation/api-reference/protocol-v4/get-a-dispute
https://api.storyapis.com/api/v4/openapi.json get /disputes/{disputeId}
Retrieve a single dispute by its ID from the SOS database. This endpoint provides detailed information about a specific dispute including its status, involved parties, and related metadata.
# Get License Tokens
Source: https://docs.story.foundation/api-reference/protocol-v4/get-license-tokens
https://api.storyapis.com/api/v4/openapi.json post /licenses/tokens
Retrieve license tokens with optional filtering. If no filters are provided, returns all license tokens. Supports filtering by owner wallet address and/or licensor IP ID. Results are paginated and can be ordered by 'blockNumber' (default: descending).
# List Collections
Source: https://docs.story.foundation/api-reference/protocol-v4/list-collections
https://api.storyapis.com/api/v4/openapi.json post /collections
Retrieve a list of collections with pagination and filtering options. Collections can be ordered by updatedAt, assetCount, or licensesCount (asc/desc). Collections are automatically enriched with metadata. The 'where' field is optional and should only be provided when filtering by specific collection addresses or asset counts. This endpoint can also be used to fetch a single collection by passing its address in the collectionAddresses filter. Collections that don't exist in Alchemy or encounter errors will return with empty metadata instead of failing the entire request.
# List Disputes
Source: https://docs.story.foundation/api-reference/protocol-v4/list-disputes
https://api.storyapis.com/api/v4/openapi.json post /disputes
Retrieve a paginated, filtered list of disputes from the SOS database.
This endpoint supports various filtering options including:
- Filter by dispute ID, target IP ID, initiator address, or status
- Filter by block number ranges (blockNumber, blockNumberLte, blockNumberGte)
- Sort by block number, dispute timestamp, or ID
- Pagination with configurable limit and offset
Key v4 Enhancements:
- Flattened request structure: Query parameters are directly in the request body, not nested in an "options" object
- Offset-based pagination: Unlike v3, this endpoint supports both limit and offset parameters for flexible pagination
- Block number range filtering: Added blockNumberGte for greater-than-or-equal filtering (complements existing blockNumberLte)
- Pagination metadata in response: Returns total count and hasMore flag
The response format is aligned with v3 for compatibility, using big integers for numeric fields and hex strings for data fields.
# List IP Asset Edges
Source: https://docs.story.foundation/api-reference/protocol-v4/list-ip-asset-edges
https://api.storyapis.com/api/v4/openapi.json post /assets/edges
Retrieve a list of edges (derivative registered events) that represent relationships between IP assets. These edges show parent-child relationships formed through licensing.
# List IP Assets
Source: https://docs.story.foundation/api-reference/protocol-v4/list-ip-assets
https://api.storyapis.com/api/v4/openapi.json post /assets
Retrieve a list of IP assets with pagination and filtering options. The 'where' field is optional and should only be provided when filtering by specific IP IDs, owner address, or token contract address. This endpoint can also be used to fetch a single asset by passing its ID in the ipIds filter.
# Introduction
Source: https://docs.story.foundation/api-reference/protocol/introduction
Example section for showcasing API endpoints
Welcome to the Story API Reference! See below for details.
You can use the following public API key:
```http Headers theme={null}
// mainnet
X-API-Key: MhBsxkU1z9fG6TofE59KqiiWV-YlYE8Q4awlLQehF3U
// aeneid testnet
X-API-Key: KOTbaGUSWQ6cUJWhiJYiOjPgB0kTRu1eCFFvQL0IWls
```
| Environment | Endpoint | Live Docs | OpenAPI JSON |
| -------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| Mainnet | `https://api.storyapis.com/api/v4` | [Go](https://api.storyapis.com/api/v4/docs) | [Go](https://api.storyapis.com/api/v4/openapi.json) |
| Aeneid Testnet | `https://staging-api.storyprotocol.net/api/v4` | [Go](https://staging-api.storyprotocol.net/api/v4/docs) | [Go](https://staging-api.storyprotocol.net/api/v4/openapi.json) |
## Rate Limit
The above public API key has a requests/second of 300. If you'd like an API key with a higher limit, please join our Builder Discord and describe your project needs in the discussion channel.
# Search IP Assets
Source: https://docs.story.foundation/api-reference/protocol/search
https://api.storyapis.com/api/v4/openapi.json POST /search
Perform vector search for IP assets based on query text and optional media type filter. This endpoint uses AI-powered search to find relevant assets by semantic similarity.
Search is done by vectorizing the title and description of the IP metadata using the [IPA metadata standard](https://docs.story.foundation/concepts/ip-asset/ipa-metadata-standard).
# List IP Transactions
Source: https://docs.story.foundation/api-reference/protocol/transactions
https://api.storyapis.com/api/v4/openapi.json POST /transactions
Retrieve a list of IP transactions with pagination and filtering options. The ‘where’ field is optional and should only be provided when filtering by specific transaction hashes, event types, or block ranges. This endpoint can also be used to fetch specific transactions by passing their hashes in the txHashes filter.
This endpoint allows you to filter based on event type. Here are the possible string values for the `where.eventTypes[]` field:
* "IPRegistered": When an IP asset is registered
* "LicenseTermsAttached": When license terms are attached to an IP asset
* "DerivativeRegistered": When a derivative IP asset is registered
* "DisputeRaised": When a dispute is raised against an IP asset
* "DisputeResolved": When a dispute is resolved
* "DisputeCancelled": When a dispute is cancelled
* "DisputeJudgementSet": When a judgement is set for a dispute
* "RoyaltyPaid": When royalty payments are made
# Global Wallet
Source: https://docs.story.foundation/developers/global-wallet/overview
Use Story's Global Wallet to enable Dynamic social login in your app.
The Story Global Wallet is a Dynamic-powered wallet experience that you can enable by adding a single import to your app.
## Integrate
Install the package in your app (see the [npm page](https://www.npmjs.com/package/@story-protocol/global-wallet)):
```bash npm theme={null}
npm install @story-protocol/global-wallet
```
Add this import at the top of your root client component:
```tsx theme={null}
import "@story-protocol/global-wallet/story";
```
## What users get
After the import is in place, “Story Global Wallet” is automatically added to the wallet connection UI wallet list. Users can then log in via socials using Dynamic.
## Examples
See the reference implementations below. Make sure to read the [README](https://github.com/piplabs/story-global-wallet/blob/main/README.md) to get them up and running.
* [Dynamic wallet + Next.js](https://github.com/piplabs/story-global-wallet/tree/main/examples/dynamic-nextjs)
* [RainbowKit + Vite](https://github.com/piplabs/story-global-wallet/tree/main/examples/rainbowkit-vite)
# Account Systems
Source: https://docs.story.foundation/developers/infra-partners/account-system
Choose from our trusted partner integrations for embedded wallets and account
abstraction to simplify user onboarding and transaction management in your
Story Protocol application.
Simplify user onboarding with embedded wallet solutions that abstract away private key management.
Easy-to-use embedded wallets with social login support
Multi-chain wallet infrastructure with customizable UI
Secure embedded wallets with biometric authentication
Web3 development platform with built-in wallet solutions
Enable gasless transactions and improved UX with account abstraction providers.
ERC-4337 account abstraction infrastructure
Smart contract wallets with account abstraction
Alchemy account abstraction infrastructure
# Indexers
Source: https://docs.story.foundation/developers/infra-partners/indexers
Goldsky is the go-to data indexer for web3 builders, providing high-performance subgraph hosting and real-time data replication pipelines.
Alchemy is a blockchain indexing platform that provides a comprehensive suite of tools for indexing and querying blockchain data.
# Multisig
Source: https://docs.story.foundation/developers/infra-partners/multisig
Multisig wallets require multiple approvals for transactions, enhancing security for digital assets. Story uses Safe, a trusted multisig solution, for transparent collective management of its onchain operations.
## Story Safe
Users can access Safe via a user-friendly web UI or directly through smart contracts, giving both regular users and developers flexible options for managing multisig wallets.
### Using Safe's Web App
Use Safe’s intuitive web app to easily propose, review, and execute multisig transactions — no coding required!
Go to the web app and try it out.
### Smart Contract Integration
For direct interaction with Safe's smart contracts, see the official documentation for technical guidance and examples to build or automate Safe workflows.
Check out Safe's documentation to learn more.
# Gelato
Source: https://docs.story.foundation/developers/infra-partners/oracles/gelato
Gelato Network automates smart contract execution across major blockchains. Its products include decentralized automation, Web3 Functions, Gasless Transactions, and Gelato VRF for secure, verifiable randomness.
# VRF
Gelato VRF provides verifiable randomness for blockchain applications by utilizing Drand, a decentralized and trusted source of random numbers. It ensures that developers receive truly random values that are both provable and tamper-resistant.
See Gelato's [Documentation](https://docs.gelato.network/web3-services/vrf) guide to integrate your application with their price feeds.
## Smart Contracts
### Functions and VRF
#### Mainnet
##### [EIP173Proxy.sol](https://www.storyscan.io/address/0xafd37d0558255aA687167560cd3AaeEa75c2841E)
```
0xafd37d0558255aA687167560cd3AaeEa75c2841E
```
##### [Automate.sol](https://www.storyscan.io/address/0xab2c44495F5F954149b94C750ca20B64ea60B51c)
```
0xab2c44495F5F954149b94C750ca20B64ea60B51c
```
### Relays
#### Mainnet
##### GelatoRelay
Relay method: `callWithSyncFee`
###### EIP173Proxy.sol
```
0xcd565435e0d2109feFde337a66491541Df0D1420
```
#####
###### GelatoRelay.sol
```
0xA75983F686999843804a2ECC0E93C35d39a4F750
```
##### GelatoRelayERC2771.sol
Relay method: `callWithSyncFeeERC2771`
```
0x8aCE64CEA52b409F930f60B516F65197faD4B056
```
##### GelatoRelayConcurrentERC2771.sol
Relay method: `callWithSyncFeeERC2771` with `isConcurrent: true`
```
0xc7739c195618D314C08E8626C98f8573E4E43634
```
##### GelatoRelay1BalanceERC2771.sol
Relay method: `sponsoredCallERC2771`
```
0x61F2976610970AFeDc1d83229e1E21bdc3D5cbE4
```
# Pyth
Source: https://docs.story.foundation/developers/infra-partners/oracles/pyth
Pyth Network is a decentralized oracle providing pricing data and verifiable random functions (VRF) for smart contracts. By sourcing data directly from institutional providers, Pyth ensures secure, low-latency price updates while maintaining transparency and efficiency.
# Price Feeds
Pyth Network provides real-time financial market data to smart contracts across 100+ blockchains, sourcing prices from over 100 exchanges and market makers. With 850+ price feeds covering equities, commodities, and cryptocurrencies, Pyth aggregates and updates prices multiple times per second.
See Pyth's [Documentation](https://docs.pyth.network/price-feeds/price-feeds) guide to integrate your application with their price feeds.
## Smart Contracts
### Mainnet
#### [ERC1967Proxy.sol](https://www.storyscan.io/address/0xD458261E832415CFd3BAE5E416FdF3230ce6F134)
```
0xD458261E832415CFd3BAE5E416FdF3230ce6F134
```
#### [PythUpgradable.sol](https://www.storyscan.io/address/0x5f3c61944CEb01B3eAef861251Fb1E0f14b848fb)
```
0x5f3c61944CEb01B3eAef861251Fb1E0f14b848fb
```
### Testnet (Aeneid)
#### [ERC1967Proxy.sol](https://aeneid.storyscan.io/address/0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320)
```
0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320
```
#### [PythUpgradeable.sol](https://aeneid.storyscan.io/address/0x98046Bd286715D3B0BC227Dd7a956b83D8978603)
```
0x98046Bd286715D3B0BC227Dd7a956b83D8978603
```
# VRF
Pyth Entropy(VRF) service enables on-chain generation of provably fair random numbers. To integrate Pyth Entropy, you need to invoke an on-chain function to request a random number from Entropy. This function accepts a randomly generated number, which can be created off-chain and sent to the Entropy contract. In return, the contract provides a sequence number. Once the request is processed, Pyth Entropy will send a callback to your contract, delivering the generated random number.
See Pyth's [How to Generate Random numbers in EVM dApps](https://docs.pyth.network/entropy/generate-random-numbers/evm) guide to integrate your application with Pyth Entropy.
## Smart Contracts
### Mainnet
#### [ERC1967Proxy.sol](https://www.storyscan.io/address/0xdF21D137Aadc95588205586636710ca2890538d5)
```
0xdF21D137Aadc95588205586636710ca2890538d5
```
#### [EntropyUpgradeable.sol](https://www.storyscan.io/address/0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF)
```
0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF
```
### Testnet (Aeneid)
#### [ERC1967Proxy.sol](https://aeneid.storyscan.io/address/0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb)
```
0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb
```
#### [EntropyUpgradeable.sol](https://aeneid.storyscan.io/address/0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E)
```
0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E
```
# Redstone
Source: https://docs.story.foundation/developers/infra-partners/oracles/redstone
Redstone is a decentralized oracle network designed to deliver customizable data feeds for smart contracts and DeFi protocols. It uses a unique off-chain data delivery model where data is signed and bundled off-chain, then submitted on-chain only when needed.
# Price Feeds
Redstone delivers real-time financial data to smart contracts on 70+ blockchains, covering crypto, RWAs, LRTs, BTCFi, and other emerging assets. Combining institutional and crypto-native data, Redstone ensures reliability through multi-layered validation, including anomaly detection, market depth analysis, and cross-source variance checks.
See Redstone's [Documentation](https://docs.redstone.finance/docs/introduction) guide to integrate your application with their price feeds.
## Smart Contracts
### ETH
#### TransparentUpgradeableProxy
```
0x22d47686b3AEC9068768f84EFD8Ce2637a347B0A
```
#### StoryPriceFeedEthWithoutRoundsV1
```
0xb9D0073aCb296719C26a8BF156e4b599174fe1d5
```
### BTC
#### TransparentUpgradeableProxy
```
0xc44be6D00307c3565FDf753e852Fc003036cBc13
```
#### StoryPriceFeedBtcWithoutRoundsV1
```
0xE23eCA12D7D2ED3829499556F6dCE06642AFd990
```
### USDC
#### TransparentUpgradeableProxy
```
0xED2B1ca5D7E246f615c2291De309643D41FeC97e
```
#### StoryPriceFeedUsdcWithoutRoundsV1
```
0x31a36CdF4465ba61ce78F5CDbA26FDF8ec361803
```
### USDT
#### TransparentUpgradeableProxy
```
0x7A9b672fc20b5C89D6774514052b3e0899E5E263
```
#### StoryPriceFeedUsdtWithoutRoundsV1
```
0xe8D9FbC10e00ecc9f0694617075fDAF657a76FB2
```
# Overview
Source: https://docs.story.foundation/developers/infra-partners/overview
Welcome to Story's infra partners page. Here you'll find a comprehensive list of partners and solutions building on Sophon, categorized by their primary focus areas:
Access points that enable direct interaction with the Story blockchain,
providing reliable infrastructure for reading chain data and submitting
transactions.
Solutions for managing user identities, wallets, and authentication, making it
easier for users to interact with Story applications.
Services that process and organize blockchain data into easily queryable
formats, enabling efficient data retrieval and analysis.
Trusted data feeds that connect Story to off-chain information, enabling smart
contracts to interact with real-world data.
Verifiable random number generation services that provide secure and
unpredictable values for gaming, NFTs, and other applications.
Infrastructure for handling transactions, fiat on/off ramps, and payment
processing to enable seamless financial interactions.
# Payment Solutions
Source: https://docs.story.foundation/developers/infra-partners/payment-solutions
Halliday is the first workflow protocol. Automate any workflow and manage reliability, cross-chain execution, business logic and more. Create breakthrough applications in hours not years.
Transak enables users to buy or sell crypto from your app. Available across
170 cryptocurrencies on 80+ blockchains via cards, bank transfers and other
payment methods in 64 countries.
Alchemy Pay enables your users to buy and sell digital assets with fiat
currencies on your platform. Empower your project for global adoption.
# Randomness
Source: https://docs.story.foundation/developers/infra-partners/randomness
Gelato is a decentralized infrastructure provider, for this collaboration, user can use gelato for VRF on-chain randomness.
Pyth entropy is a decentralized randomness oracle that provides random numbers for blockchain applications.
# RPC Provider
Source: https://docs.story.foundation/developers/infra-partners/rpc-provider
Alchemy is a developer platform offering advanced APIs and analytics for
building on blockchain.
QuickNode is a high-performance Web3 infrastructure with fast, reliable RPC
across major chains.
Ankr is a blockchain infrastructure provider that provides a comprehensive suite of tools for indexing and querying blockchain data.
# Attach Terms to an IPA
Source: https://docs.story.foundation/developers/smart-contracts-guide/attach-terms
Learn how to attach License Terms to an IP Asset in Solidity.
Follow the completed code all the way through.
This section demonstrates how to attach [License Terms](/concepts/licensing-module/license-terms) to an [IP Asset](/concepts/ip-asset/overview). By attaching terms, users can publicly mint [License Tokens](/concepts/licensing-module/license-token) (the on-chain "license") with those terms from the IP.
## Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup)
2. Create License Terms and have a `licenseTermsId`. You can do that by following the [previous page](/developers/smart-contracts-guide/register-terms).
## Attach License Terms
Now that we have created terms and have the associated `licenseTermsId`, we can attach them to an existing IP Asset.
Let's create a test file under `test/2_AttachTerms.t.sol` to see it work and verify the results:
**Contract Addresses**
We have filled in the addresses from the Story contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts)
```solidity test/2_AttachTerms.t.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
// for testing purposes only
import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol";
import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol";
import { ILicenseRegistry } from "@storyprotocol/core/interfaces/registries/ILicenseRegistry.sol";
import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol";
import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol";
import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { SimpleNFT } from "../src/mocks/SimpleNFT.sol";
// Run this test:
// forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/2_AttachTerms.t.sol
contract AttachTermsTest is Test {
address internal alice = address(0xa11ce);
// For addresses, see https://docs.story.foundation/developers/deployed-smart-contracts
// Protocol Core - IPAssetRegistry
IIPAssetRegistry internal IP_ASSET_REGISTRY = IIPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b);
// Protocol Core - LicenseRegistry
ILicenseRegistry internal LICENSE_REGISTRY = ILicenseRegistry(0x529a750E02d8E2f15649c13D69a465286a780e24);
// Protocol Core - LicensingModule
ILicensingModule internal LICENSING_MODULE = ILicensingModule(0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f);
// Protocol Core - PILicenseTemplate
IPILicenseTemplate internal PIL_TEMPLATE = IPILicenseTemplate(0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316);
// Protocol Core - RoyaltyPolicyLAP
address internal ROYALTY_POLICY_LAP = 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E;
// Revenue Token - MERC20
address internal MERC20 = 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E;
SimpleNFT public SIMPLE_NFT;
uint256 public tokenId;
address public ipId;
uint256 public licenseTermsId;
function setUp() public {
// this is only for testing purposes
// due to our IPGraph precompile not being
// deployed on the fork
vm.etch(address(0x0101), address(new MockIPGraph()).code);
SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM");
tokenId = SIMPLE_NFT.mint(alice);
ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId);
// Register random Commercial Remix terms so we can attach them later
licenseTermsId = PIL_TEMPLATE.registerLicenseTerms(
PILFlavors.commercialRemix({
mintingFee: 0,
commercialRevShare: 10 * 10 ** 6, // 10%
royaltyPolicy: ROYALTY_POLICY_LAP,
currencyToken: MERC20
})
);
}
/// @notice Attaches license terms to an IP Asset.
/// @dev Only the owner of an IP Asset can attach license terms to it.
/// So in this case, alice has to be the caller of the function because
/// she owns the NFT associated with the IP Asset.
function test_attachLicenseTerms() public {
vm.prank(alice);
LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId);
assertTrue(LICENSE_REGISTRY.hasIpAttachedLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId));
assertEq(LICENSE_REGISTRY.getAttachedLicenseTermsCount(ipId), 1);
(address licenseTemplate, uint256 attachedLicenseTermsId) = LICENSE_REGISTRY.getAttachedLicenseTerms({
ipId: ipId,
index: 0
});
assertEq(licenseTemplate, address(PIL_TEMPLATE));
assertEq(attachedLicenseTermsId, licenseTermsId);
}
}
```
## Test Your Code!
Run `forge build`. If everything is successful, the command should successfully compile.
Now run the test by executing the following command:
```bash theme={null}
forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/2_AttachTerms.t.sol
```
## Mint a License
Congratulations, you attached terms to an IPA!
Follow the completed code all the way through.
Now that we have attached License Terms to our IP, the next step is minting a License Token, which we'll go over on the next page.
# Pay & Claim Revenue
Source: https://docs.story.foundation/developers/smart-contracts-guide/claim-revenue
Learn how to pay an IP Asset and claim revenue in Solidity.
Follow the completed code all the way through.
This section demonstrates how to pay an IP Asset. There are a few reasons you would do this:
1. You simply want to "tip" an IP
2. You have to because your license terms with an ancestor IP require you to forward a certain % of payment
In either scenario, you would use the below `payRoyaltyOnBehalf` function. When this happens, the [Royalty Module](/concepts/royalty-module/overview) automatically handles the different payment flows such that parent IP Assets who have negotiated a certain `commercialRevShare` with the IPA being paid can claim their due share.
## Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup)
2. Have a basic understanding of the [Royalty Module](/concepts/royalty-module/overview)
3. A child IPA and a parent IPA, for which their license terms have a commercial revenue share to make this example work
## Before We Start
You can pay an IP Asset using the `payRoyaltyOnBehalf` function from the [Royalty Module](/concepts/royalty-module/overview).
You will be paying the IP Asset with [MockERC20](https://aeneid.storyscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E). Usually you would pay with \$WIP, but because we need to mint some tokens to test, we will use MockERC20.
To help with the following scenarios, let's say we have a parent IP Asset that has negotiated a 50% `commercialRevShare` with its child IP Asset.
### Whitelisted Revenue Tokens
Only tokens that are whitelisted by our protocol can be used as payment ("revenue") tokens. MockERC20 is one of those tokens. To see that list, go [here](/developers/deployed-smart-contracts#whitelisted-revenue-tokens).
## Paying an IP Asset
We can pay an IP Asset like so:
```solidity Solidity theme={null}
ROYALTY_MODULE.payRoyaltyOnBehalf(childIpId, address(0), address(MERC20), 10);
```
This will send 10 \$MERC20 to the `childIpId`'s [IP Royalty Vault](/concepts/royalty-module/ip-royalty-vault). From there, the child can claim revenue. In the next section, you'll see a working version of this.
**Important: Approving the Royalty Module**
Before you call `payRoyaltyOnBehalf`, you have to approve the royalty module to spend the tokens for you. In the section below, you will see that we call `MERC20.approve(address(ROYALTY_MODULE), 10);` or else it will not work.
## Claim Revenue
When payments are made, they eventually end up in an IP Asset's [IP Royalty Vault](/concepts/royalty-module/ip-royalty-vault). From here, they are claimed/transferred to whoever owns the Royalty Tokens associated with it, which represent a % of revenue share for a given IP Asset's IP Royalty Vault.
The IP Account (the smart contract that represents the [IP Asset](/concepts/ip-asset/overview)) is what holds 100% of the Royalty Tokens when it's first registered. So usually, it indeed holds most of the Royalty Tokens.
Let's create a test file under `test/5_Royalty.t.sol` to see it work and verify the results:
**Contract Addresses**
We have filled in the addresses from the Story contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts)
```solidity test/5_Royalty.t.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
// for testing purposes only
import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol";
import { IPAssetRegistry } from "@storyprotocol/core/registries/IPAssetRegistry.sol";
import { LicenseRegistry } from "@storyprotocol/core/registries/LicenseRegistry.sol";
import { PILicenseTemplate } from "@storyprotocol/core/modules/licensing/PILicenseTemplate.sol";
import { RoyaltyPolicyLAP } from "@storyprotocol/core/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol";
import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol";
import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { LicensingModule } from "@storyprotocol/core/modules/licensing/LicensingModule.sol";
import { LicenseToken } from "@storyprotocol/core/LicenseToken.sol";
import { RoyaltyWorkflows } from "@storyprotocol/periphery/workflows/RoyaltyWorkflows.sol";
import { RoyaltyModule } from "@storyprotocol/core/modules/royalty/RoyaltyModule.sol";
import { MockERC20 } from "@storyprotocol/test/mocks/token/MockERC20.sol";
import { SimpleNFT } from "../src/mocks/SimpleNFT.sol";
// Run this test:
// forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/5_Royalty.t.sol
contract RoyaltyTest is Test {
address internal alice = address(0xa11ce);
address internal bob = address(0xb0b);
// For addresses, see https://docs.story.foundation/developers/deployed-smart-contracts
// Protocol Core - IPAssetRegistry
IPAssetRegistry internal IP_ASSET_REGISTRY = IPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b);
// Protocol Core - LicenseRegistry
LicenseRegistry internal LICENSE_REGISTRY = LicenseRegistry(0x529a750E02d8E2f15649c13D69a465286a780e24);
// Protocol Core - LicensingModule
LicensingModule internal LICENSING_MODULE = LicensingModule(0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f);
// Protocol Core - PILicenseTemplate
PILicenseTemplate internal PIL_TEMPLATE = PILicenseTemplate(0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316);
// Protocol Core - RoyaltyPolicyLAP
RoyaltyPolicyLAP internal ROYALTY_POLICY_LAP = RoyaltyPolicyLAP(0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E);
// Protocol Core - LicenseToken
LicenseToken internal LICENSE_TOKEN = LicenseToken(0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC);
// Protocol Core - RoyaltyModule
RoyaltyModule internal ROYALTY_MODULE = RoyaltyModule(0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086);
// Protocol Periphery - RoyaltyWorkflows
RoyaltyWorkflows internal ROYALTY_WORKFLOWS = RoyaltyWorkflows(0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890);
// Mock - MERC20
MockERC20 internal MERC20 = MockERC20(0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E);
SimpleNFT public SIMPLE_NFT;
uint256 public tokenId;
address public ipId;
uint256 public licenseTermsId;
uint256 public startLicenseTokenId;
address public childIpId;
function setUp() public {
// this is only for testing purposes
// due to our IPGraph precompile not being
// deployed on the fork
vm.etch(address(0x0101), address(new MockIPGraph()).code);
SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM");
tokenId = SIMPLE_NFT.mint(alice);
ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId);
licenseTermsId = PIL_TEMPLATE.registerLicenseTerms(
PILFlavors.commercialRemix({
mintingFee: 0,
commercialRevShare: 10 * 10 ** 6, // 10%
royaltyPolicy: address(ROYALTY_POLICY_LAP),
currencyToken: address(MERC20)
})
);
vm.prank(alice);
LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId);
startLicenseTokenId = LICENSING_MODULE.mintLicenseTokens({
licensorIpId: ipId,
licenseTemplate: address(PIL_TEMPLATE),
licenseTermsId: licenseTermsId,
amount: 2,
receiver: bob,
royaltyContext: "", // for PIL, royaltyContext is empty string
maxMintingFee: 0,
maxRevenueShare: 0
});
// Registers a child IP (owned by Bob) as a derivative of Alice's IP.
uint256 childTokenId = SIMPLE_NFT.mint(bob);
childIpId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), childTokenId);
uint256[] memory licenseTokenIds = new uint256[](1);
licenseTokenIds[0] = startLicenseTokenId;
vm.prank(bob);
LICENSING_MODULE.registerDerivativeWithLicenseTokens({
childIpId: childIpId,
licenseTokenIds: licenseTokenIds,
royaltyContext: "", // empty for PIL
maxRts: 0
});
}
/// @notice Pays MERC20 to Bob's IP. Some of this MERC20 is then claimable
/// by Alice's IP.
/// @dev In this case, this contract will act as the 3rd party paying MERC20
/// to Bob (the child IP).
function test_claimAllRevenue() public {
// ADMIN SETUP
// We mint 100 MERC20 to this contract so it has some money to pay.
MERC20.mint(address(this), 100);
// We have to approve the Royalty Module to spend MERC20 on our behalf, which
// it will do using `payRoyaltyOnBehalf`.
MERC20.approve(address(ROYALTY_MODULE), 10);
// This contract pays 10 MERC20 to Bob's IP.
ROYALTY_MODULE.payRoyaltyOnBehalf(childIpId, address(0), address(MERC20), 10);
// Now that Bob's IP has been paid, Alice can claim her share (1 MERC20, which
// is 10% as specified in the license terms)
address[] memory childIpIds = new address[](1);
address[] memory royaltyPolicies = new address[](1);
address[] memory currencyTokens = new address[](1);
childIpIds[0] = childIpId;
royaltyPolicies[0] = address(ROYALTY_POLICY_LAP);
currencyTokens[0] = address(MERC20);
uint256[] memory amountsClaimed = ROYALTY_WORKFLOWS.claimAllRevenue({
ancestorIpId: ipId,
claimer: ipId,
childIpIds: childIpIds,
royaltyPolicies: royaltyPolicies,
currencyTokens: currencyTokens
});
// Check that 1 MERC20 was claimed by Alice's IP Account
assertEq(amountsClaimed[0], 1);
// Check that Alice's IP Account now has 1 MERC20 in its balance.
assertEq(MERC20.balanceOf(ipId), 1);
// Check that Bob's IP now has 9 MERC20 in its Royalty Vault, which it
// can claim to its IP Account at a later point if he wants.
assertEq(MERC20.balanceOf(ROYALTY_MODULE.ipRoyaltyVaults(childIpId)), 9);
}
}
```
## Test Your Code!
Run `forge build`. If everything is successful, the command should successfully compile.
Now run the test by executing the following command:
```bash theme={null}
forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/5_Royalty.t.sol
```
## Dispute an IP
Congratulations, you claimed revenue using the [Royalty Module](/concepts/royalty-module/overview)!
Follow the completed code all the way through.
Now what happens if an IP Asset doesn't pay their due share? We can dispute the IP on-chain, which we will cover on the next page.
Coming soon!
# Using an Example
Source: https://docs.story.foundation/developers/smart-contracts-guide/example
Combine all of our tutorials together in a practical example.
See the completed code.
Check out a video walkthrough of this tutorial!
# Writing the Smart Contract
Now that we have walked through each of the individual steps, let's try to write, deploy, and verify our own smart contract.
## Register IPA, Register License Terms, and Attach to IPA
In this first section, we will combine a few of the tutorials into one. We will create a function named `mintAndRegisterAndCreateTermsAndAttach` that allows you to mint & register a new IP Asset, register new License Terms, and attach those terms to an IP Asset. It will also accept a `receiver` field to be the owner of the new IP Asset.
### Prerequisites
* Complete [Register an IP Asset](/developers/smart-contracts-guide/register-ip-asset)
* Complete [Register License Terms](/developers/smart-contracts-guide/register-terms)
* Complete [Attach Terms to an IPA](/developers/smart-contracts-guide/attach-terms)
### Writing our Contract
Create a new file under `./src/Example.sol` and paste the following:
**Contract Addresses**
In order to get the contract addresses to pass in the constructor, go to [Deployed Smart Contracts](/developers/deployed-smart-contracts).
```solidity src/Example.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol";
import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol";
import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol";
import { SimpleNFT } from "./mocks/SimpleNFT.sol";
import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
/// @notice An example contract that demonstrates how to mint an NFT, register it as an IP Asset,
/// attach license terms to it, mint a license token from it, and register it as a derivative of the parent.
contract Example is ERC721Holder {
IIPAssetRegistry public immutable IP_ASSET_REGISTRY;
ILicensingModule public immutable LICENSING_MODULE;
IPILicenseTemplate public immutable PIL_TEMPLATE;
address public immutable ROYALTY_POLICY_LAP;
address public immutable WIP;
SimpleNFT public immutable SIMPLE_NFT;
constructor(
address ipAssetRegistry,
address licensingModule,
address pilTemplate,
address royaltyPolicyLAP,
address wip
) {
IP_ASSET_REGISTRY = IIPAssetRegistry(ipAssetRegistry);
LICENSING_MODULE = ILicensingModule(licensingModule);
PIL_TEMPLATE = IPILicenseTemplate(pilTemplate);
ROYALTY_POLICY_LAP = royaltyPolicyLAP;
WIP = wip;
// Create a new Simple NFT collection
SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM");
}
/// @notice Mint an NFT, register it as an IP Asset, and attach License Terms to it.
/// @param receiver The address that will receive the NFT/IPA.
/// @return tokenId The token ID of the NFT representing ownership of the IPA.
/// @return ipId The address of the IP Account.
/// @return licenseTermsId The ID of the license terms.
function mintAndRegisterAndCreateTermsAndAttach(
address receiver
) external returns (uint256 tokenId, address ipId, uint256 licenseTermsId) {
// We mint to this contract so that it has permissions
// to attach license terms to the IP Asset.
// We will later transfer it to the intended `receiver`
tokenId = SIMPLE_NFT.mint(address(this));
ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId);
// register license terms so we can attach them later
licenseTermsId = PIL_TEMPLATE.registerLicenseTerms(
PILFlavors.commercialRemix({
mintingFee: 0,
commercialRevShare: 10 * 10 ** 6, // 10%
royaltyPolicy: ROYALTY_POLICY_LAP,
currencyToken: WIP
})
);
// attach the license terms to the IP Asset
LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId);
// transfer the NFT to the receiver so it owns the IPA
SIMPLE_NFT.transferFrom(address(this), receiver, tokenId);
}
}
```
## Mint a License Token and Register as Derivative
In this next section, we will combine a few of the later tutorials into one. We will create a function named `mintLicenseTokenAndRegisterDerivative` that allows a potentially different user to register their own "child" (derivative) IP Asset, mint a License Token from the "parent" (root) IP Asset, and register their child IPA as a derivative of the parent IPA. It will accept a few parameters:
1. `parentIpId`: the `ipId` of the parent IPA
2. `licenseTermsId`: the id of the License Terms you want to mint a License Token for
3. `receiver`: the owner of the child IPA
### Prerequisites
* Complete [Mint a License Token](/developers/smart-contracts-guide/mint-license)
* Complete [Register a Derivative](/developers/smart-contracts-guide/register-derivative)
### Writing our Contract
In your `Example.sol` contract, add the following function at the bottom:
```solidity src/Example.sol theme={null}
/// @notice Mint and register a new child IPA, mint a License Token
/// from the parent, and register it as a derivative of the parent.
/// @param parentIpId The ipId of the parent IPA.
/// @param licenseTermsId The ID of the license terms you will
/// mint a license token from.
/// @param receiver The address that will receive the NFT/IPA.
/// @return childTokenId The token ID of the NFT representing ownership of the child IPA.
/// @return childIpId The address of the child IPA.
function mintLicenseTokenAndRegisterDerivative(
address parentIpId,
uint256 licenseTermsId,
address receiver
) external returns (uint256 childTokenId, address childIpId) {
// We mint to this contract so that it has permissions
// to register itself as a derivative of another
// IP Asset.
// We will later transfer it to the intended `receiver`
childTokenId = SIMPLE_NFT.mint(address(this));
childIpId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), childTokenId);
// mint a license token from the parent
uint256 licenseTokenId = LICENSING_MODULE.mintLicenseTokens({
licensorIpId: parentIpId,
licenseTemplate: address(PIL_TEMPLATE),
licenseTermsId: licenseTermsId,
amount: 1,
// mint the license token to this contract so it can
// use it to register as a derivative of the parent
receiver: address(this),
royaltyContext: "", // for PIL, royaltyContext is empty string
maxMintingFee: 0,
maxRevenueShare: 0
});
uint256[] memory licenseTokenIds = new uint256[](1);
licenseTokenIds[0] = licenseTokenId;
// register the new child IPA as a derivative
// of the parent
LICENSING_MODULE.registerDerivativeWithLicenseTokens({
childIpId: childIpId,
licenseTokenIds: licenseTokenIds,
royaltyContext: "", // empty for PIL
maxRts: 0
});
// transfer the NFT to the receiver so it owns the child IPA
SIMPLE_NFT.transferFrom(address(this), receiver, childTokenId);
}
```
# Testing our Contract
Create another new file under `test/Example.t.sol` and paste the following:
```solidity test/Example.t.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
// for testing purposes only
import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol";
import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol";
import { ILicenseRegistry } from "@storyprotocol/core/interfaces/registries/ILicenseRegistry.sol";
import { Example } from "../src/Example.sol";
import { SimpleNFT } from "../src/mocks/SimpleNFT.sol";
// Run this test:
// forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/Example.t.sol
contract ExampleTest is Test {
address internal alice = address(0xa11ce);
address internal bob = address(0xb0b);
// For addresses, see https://docs.story.foundation/developers/deployed-smart-contracts
// Protocol Core - IPAssetRegistry
address internal ipAssetRegistry = 0x77319B4031e6eF1250907aa00018B8B1c67a244b;
// Protocol Core - LicenseRegistry
address internal licenseRegistry = 0x529a750E02d8E2f15649c13D69a465286a780e24;
// Protocol Core - LicensingModule
address internal licensingModule = 0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f;
// Protocol Core - PILicenseTemplate
address internal pilTemplate = 0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316;
// Protocol Core - RoyaltyPolicyLAP
address internal royaltyPolicyLAP = 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E;
// Revenue Token - WIP
address internal wip = 0x1514000000000000000000000000000000000000;
SimpleNFT public SIMPLE_NFT;
Example public EXAMPLE;
function setUp() public {
// this is only for testing purposes
// due to our IPGraph precompile not being
// deployed on the fork
vm.etch(address(0x0101), address(new MockIPGraph()).code);
EXAMPLE = new Example(ipAssetRegistry, licensingModule, pilTemplate, royaltyPolicyLAP, wip);
SIMPLE_NFT = SimpleNFT(EXAMPLE.SIMPLE_NFT());
}
function test_mintAndRegisterAndCreateTermsAndAttach() public {
ILicenseRegistry LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry);
IIPAssetRegistry IP_ASSET_REGISTRY = IIPAssetRegistry(ipAssetRegistry);
uint256 expectedTokenId = SIMPLE_NFT.nextTokenId();
address expectedIpId = IP_ASSET_REGISTRY.ipId(block.chainid, address(SIMPLE_NFT), expectedTokenId);
(uint256 tokenId, address ipId, uint256 licenseTermsId) = EXAMPLE.mintAndRegisterAndCreateTermsAndAttach(alice);
assertEq(tokenId, expectedTokenId);
assertEq(ipId, expectedIpId);
assertEq(SIMPLE_NFT.ownerOf(tokenId), alice);
assertTrue(LICENSE_REGISTRY.hasIpAttachedLicenseTerms(ipId, pilTemplate, licenseTermsId));
assertEq(LICENSE_REGISTRY.getAttachedLicenseTermsCount(ipId), 1);
(address licenseTemplate, uint256 attachedLicenseTermsId) = LICENSE_REGISTRY.getAttachedLicenseTerms({
ipId: ipId,
index: 0
});
assertEq(licenseTemplate, pilTemplate);
assertEq(attachedLicenseTermsId, licenseTermsId);
}
function test_mintLicenseTokenAndRegisterDerivative() public {
ILicenseRegistry LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry);
IIPAssetRegistry IP_ASSET_REGISTRY = IIPAssetRegistry(ipAssetRegistry);
(uint256 parentTokenId, address parentIpId, uint256 licenseTermsId) = EXAMPLE
.mintAndRegisterAndCreateTermsAndAttach(alice);
(uint256 childTokenId, address childIpId) = EXAMPLE.mintLicenseTokenAndRegisterDerivative(
parentIpId,
licenseTermsId,
bob
);
assertTrue(LICENSE_REGISTRY.hasDerivativeIps(parentIpId));
assertTrue(LICENSE_REGISTRY.isParentIp(parentIpId, childIpId));
assertTrue(LICENSE_REGISTRY.isDerivativeIp(childIpId));
assertEq(LICENSE_REGISTRY.getDerivativeIpCount(parentIpId), 1);
assertEq(LICENSE_REGISTRY.getParentIpCount(childIpId), 1);
assertEq(LICENSE_REGISTRY.getParentIp({ childIpId: childIpId, index: 0 }), parentIpId);
assertEq(LICENSE_REGISTRY.getDerivativeIp({ parentIpId: parentIpId, index: 0 }), childIpId);
}
}
```
Run `forge build`. If everything is successful, the command should successfully compile.
To test this out, simply run the following command:
```bash theme={null}
forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/Example.t.sol
```
# Deploy & Verify the Example Contract
The `--constructor-args` come from [Deployed Smart Contracts](/developers/deployed-smart-contracts).
```bash theme={null}
forge create \
--rpc-url https://aeneid.storyrpc.io/ \
--private-key $PRIVATE_KEY \
./src/Example.sol:Example \
--legacy \
--verify \
--verifier blockscout \
--verifier-url https://aeneid.storyscan.io/api/ \
--constructor-args 0x77319B4031e6eF1250907aa00018B8B1c67a244b 0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f 0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E
```
If everything worked correctly, you should see something like `Deployed to: 0xfb0923D531C1ca54AB9ee10CB8364b23d0C7F47d` in the console. Paste that address into [the explorer](https://aeneid.storyscan.io/) and see your verified contract!
# Great job! :)
See the completed code.
Check out a video walkthrough of this tutorial!
# Mint a License Token
Source: https://docs.story.foundation/developers/smart-contracts-guide/mint-license
Learn how to mint a License Token from an IPA in Solidity.
Follow the completed code all the way through.
This section demonstrates how to mint a [License Token](/concepts/licensing-module/license-token) from an [IP Asset](/concepts/ip-asset/overview). You can only mint a License Token from an IP Asset if the IP Asset has [License Terms](/concepts/licensing-module/license-terms) attached to it. A License Token is minted as an ERC-721.
There are two reasons you'd mint a License Token:
1. To hold the license and be able to use the underlying IP Asset as the license described (for ex. "Can use commercially as long as you provide proper attribution and share 5% of your revenue)
2. Use the license token to link another IP Asset as a derivative of it. *Note though that, as you'll see later, some SDK functions don't require you to explicitly mint a license token first in order to register a derivative, and will actually handle it for you behind the scenes.*
## Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup)
2. An IP Asset has License Terms attached to it. You can learn how to do that [here](/developers/smart-contracts-guide/attach-terms)
## Mint License
Let's say that IP Asset (`ipId = 0x01`) has License Terms (`licenseTermdId = 10`) attached to it. We want to mint 2 License Tokens with those terms to a specific wallet address (`0x02`).
**Paid Licenses**
Be mindful that some IP Assets may have license terms attached that require the user minting the license to pay a `mintingFee`.
Let's create a test file under `test/3_LicenseToken.t.sol` to see it work and verify the results:
**Contract Addresses**
We have filled in the addresses from the Story contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts)
```solidity test/3_LicenseToken.t.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
// for testing purposes only
import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol";
import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol";
import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol";
import { ILicenseToken } from "@storyprotocol/core/interfaces/ILicenseToken.sol";
import { RoyaltyPolicyLAP } from "@storyprotocol/core/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol";
import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol";
import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { SimpleNFT } from "../src/mocks/SimpleNFT.sol";
// Run this test:
// forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/3_LicenseToken.t.sol
contract LicenseTokenTest is Test {
address internal alice = address(0xa11ce);
address internal bob = address(0xb0b);
// For addresses, see https://docs.story.foundation/developers/deployed-smart-contracts
// Protocol Core - IPAssetRegistry
IIPAssetRegistry internal IP_ASSET_REGISTRY = IIPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b);
// Protocol Core - LicensingModule
ILicensingModule internal LICENSING_MODULE = ILicensingModule(0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f);
// Protocol Core - PILicenseTemplate
IPILicenseTemplate internal PIL_TEMPLATE = IPILicenseTemplate(0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316);
// Protocol Core - RoyaltyPolicyLAP
address internal ROYALTY_POLICY_LAP = 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E;
// Protocol Core - LicenseToken
ILicenseToken internal LICENSE_TOKEN = ILicenseToken(0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC);
// Revenue Token - MERC20
address internal MERC20 = 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E;
SimpleNFT public SIMPLE_NFT;
uint256 public tokenId;
address public ipId;
uint256 public licenseTermsId;
function setUp() public {
// this is only for testing purposes
// due to our IPGraph precompile not being
// deployed on the fork
vm.etch(address(0x0101), address(new MockIPGraph()).code);
SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM");
tokenId = SIMPLE_NFT.mint(alice);
ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId);
licenseTermsId = PIL_TEMPLATE.registerLicenseTerms(
PILFlavors.commercialRemix({
mintingFee: 0,
commercialRevShare: 10 * 10 ** 6, // 10%
royaltyPolicy: ROYALTY_POLICY_LAP,
currencyToken: MERC20
})
);
vm.prank(alice);
LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId);
}
/// @notice Mints license tokens for an IP Asset.
/// Anyone can mint a license token.
function test_mintLicenseToken() public {
uint256 startLicenseTokenId = LICENSING_MODULE.mintLicenseTokens({
licensorIpId: ipId,
licenseTemplate: address(PIL_TEMPLATE),
licenseTermsId: licenseTermsId,
amount: 2,
receiver: bob,
royaltyContext: "", // for PIL, royaltyContext is empty string
maxMintingFee: 0,
maxRevenueShare: 0
});
assertEq(LICENSE_TOKEN.ownerOf(startLicenseTokenId), bob);
assertEq(LICENSE_TOKEN.ownerOf(startLicenseTokenId + 1), bob);
}
}
```
## Test Your Code!
Run `forge build`. If everything is successful, the command should successfully compile.
Now run the test by executing the following command:
```bash theme={null}
forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/3_LicenseToken.t.sol
```
## Register a Derivative
Follow the completed code all the way through.
Now that we have minted a License Token, we can hold it or use it to link an IP Asset as a derivative. We will go over that on the next page.
# Register a Derivative
Source: https://docs.story.foundation/developers/smart-contracts-guide/register-derivative
Learn how to register a derivative/remix IP Asset as a child of another in Solidity.
All of this page is covered in this working code example.
Once a [License Token](/concepts/licensing-module/license-token) has been minted from an IP Asset, the owner of that token (an ERC-721 NFT) can burn it to register their own IP Asset as a derivative of the IP Asset associated with the License Token.
## Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup)
2. Have a minted License Token. You can learn how to do that [here](/developers/smart-contracts-guide/mint-license)
## Register as Derivative
Let's create a test file under `test/4_IPARemix.t.sol` to see it work and verify the results:
**Contract Addresses**
We have filled in the addresses from the Story contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts)
```solidity test/4_IPARemix.t.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
// for testing purposes only
import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol";
import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol";
import { ILicenseRegistry } from "@storyprotocol/core/interfaces/registries/ILicenseRegistry.sol";
import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol";
import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol";
import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { SimpleNFT } from "../src/mocks/SimpleNFT.sol";
// Run this test:
// forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/4_IPARemix.t.sol
contract IPARemixTest is Test {
address internal alice = address(0xa11ce);
address internal bob = address(0xb0b);
// For addresses, see https://docs.story.foundation/developers/deployed-smart-contracts
// Protocol Core - IPAssetRegistry
IIPAssetRegistry internal IP_ASSET_REGISTRY = IIPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b);
// Protocol Core - LicenseRegistry
ILicenseRegistry internal LICENSE_REGISTRY = ILicenseRegistry(0x529a750E02d8E2f15649c13D69a465286a780e24);
// Protocol Core - LicensingModule
ILicensingModule internal LICENSING_MODULE = ILicensingModule(0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f);
// Protocol Core - PILicenseTemplate
IPILicenseTemplate internal PIL_TEMPLATE = IPILicenseTemplate(0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316);
// Protocol Core - RoyaltyPolicyLAP
address internal ROYALTY_POLICY_LAP = 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E;
// Revenue Token - MERC20
address internal MERC20 = 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E;
SimpleNFT public SIMPLE_NFT;
uint256 public tokenId;
address public ipId;
uint256 public licenseTermsId;
uint256 public startLicenseTokenId;
function setUp() public {
// this is only for testing purposes
// due to our IPGraph precompile not being
// deployed on the fork
vm.etch(address(0x0101), address(new MockIPGraph()).code);
SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM");
tokenId = SIMPLE_NFT.mint(alice);
ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId);
licenseTermsId = PIL_TEMPLATE.registerLicenseTerms(
PILFlavors.commercialRemix({
mintingFee: 0,
commercialRevShare: 10 * 10 ** 6, // 10%
royaltyPolicy: ROYALTY_POLICY_LAP,
currencyToken: MERC20
})
);
vm.prank(alice);
LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId);
startLicenseTokenId = LICENSING_MODULE.mintLicenseTokens({
licensorIpId: ipId,
licenseTemplate: address(PIL_TEMPLATE),
licenseTermsId: licenseTermsId,
amount: 2,
receiver: bob,
royaltyContext: "", // for PIL, royaltyContext is empty string
maxMintingFee: 0,
maxRevenueShare: 0
});
}
/// @notice Mints an NFT to be registered as IP, and then
/// linked as a derivative of alice's asset using the
/// minted license token.
function test_registerDerivativeWithLicenseTokens() public {
// First we mint an NFT and register it as an IP Asset,
// owned by Bob.
uint256 childTokenId = SIMPLE_NFT.mint(bob);
address childIpId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), childTokenId);
uint256[] memory licenseTokenIds = new uint256[](1);
licenseTokenIds[0] = startLicenseTokenId;
// Bob uses the License Token he has from Alice's IP
// to register his IP as a derivative of Alice's IP.
vm.prank(bob);
LICENSING_MODULE.registerDerivativeWithLicenseTokens({
childIpId: childIpId,
licenseTokenIds: licenseTokenIds,
royaltyContext: "", // empty for PIL
maxRts: 0
});
assertTrue(LICENSE_REGISTRY.hasDerivativeIps(ipId));
assertTrue(LICENSE_REGISTRY.isParentIp(ipId, childIpId));
assertTrue(LICENSE_REGISTRY.isDerivativeIp(childIpId));
assertEq(LICENSE_REGISTRY.getParentIpCount(childIpId), 1);
assertEq(LICENSE_REGISTRY.getDerivativeIpCount(ipId), 1);
assertEq(LICENSE_REGISTRY.getParentIp({ childIpId: childIpId, index: 0 }), ipId);
assertEq(LICENSE_REGISTRY.getDerivativeIp({ parentIpId: ipId, index: 0 }), childIpId);
}
}
```
## Test Your Code!
Run `forge build`. If everything is successful, the command should successfully compile.
Now run the test by executing the following command:
```bash theme={null}
forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/4_IPARemix.t.sol
```
## Paying and Claiming Revenue
Congratulations, you registered a derivative IP Asset!
All of this page is covered in this working code example.
Now that we have established parent-child IP relationships, we can begin to explore payments and automated revenue share based on the license terms. We'll cover that in the upcoming pages.
# Register License Terms
Source: https://docs.story.foundation/developers/smart-contracts-guide/register-terms
Learn how to create new License Terms in Solidity.
Follow the completed code all the way through.
[License Terms](/concepts/licensing-module/license-terms) are a configurable set of values that define restrictions on licenses minted from your IP that have those terms. For example, "If you mint this license, you must share 50% of your revenue with me." You can view the full set of terms in [PIL Terms](/concepts/programmable-ip-license/pil-terms).
## Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup)
## Before We Start
It's important to know that if **License Terms already exist for the identical set of parameters you intend to create, it is unnecessary to create it again**. License Terms are protocol-wide, so you can use existing License Terms by its `licenseTermsId`.
## Register License Terms
You can view the full set of terms in [PIL Terms](/concepts/programmable-ip-license/pil-terms).
Let's create a test file under `test/1_LicenseTerms.t.sol` to see it work and verify the results:
**Contract Addresses**
We have filled in the addresses from the Story contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts)
```solidity test/1_LicenseTerms.t.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
// Run this test:
// forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/1_LicenseTerms.t.sol
contract LicenseTermsTest is Test {
address internal alice = address(0xa11ce);
// For addresses, see https://docs.story.foundation/developers/deployed-smart-contracts
// Protocol Core - PILicenseTemplate
IPILicenseTemplate internal PIL_TEMPLATE = IPILicenseTemplate(0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316);
// Protocol Core - RoyaltyPolicyLAP
address internal ROYALTY_POLICY_LAP = 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E;
// Revenue Token - MERC20
address internal MERC20 = 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E;
function setUp() public {}
/// @notice Registers new PIL Terms. Anyone can register PIL Terms.
function test_registerPILTerms() public {
PILTerms memory pilTerms = PILTerms({
transferable: true,
royaltyPolicy: ROYALTY_POLICY_LAP,
defaultMintingFee: 0,
expiration: 0,
commercialUse: true,
commercialAttribution: true,
commercializerChecker: address(0),
commercializerCheckerData: "",
commercialRevShare: 0,
commercialRevCeiling: 0,
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: true,
derivativesReciprocal: true,
derivativeRevCeiling: 0,
currency: MERC20,
uri: ""
});
uint256 licenseTermsId = PIL_TEMPLATE.registerLicenseTerms(pilTerms);
uint256 selectedLicenseTermsId = PIL_TEMPLATE.getLicenseTermsId(pilTerms);
assertEq(licenseTermsId, selectedLicenseTermsId);
}
}
```
### PIL Flavors
As you see above, you have to choose between a lot of terms.
We have convenience functions to help you register new terms. We have created [PIL Flavors](/concepts/programmable-ip-license/pil-flavors), which are pre-configured popular combinations of License Terms to help you decide what terms to use. You can view those PIL Flavors and then register terms using the following convenience functions:
Free remixing with attribution. No commercialization.
Pay to use the license with attribution, but don't have to share revenue.
Pay to use the license with attribution and pay % of revenue earned.
Free remixing and commercial use with attribution.
For example:
```solidity Solidity theme={null}
import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol";
PILTerms memory pilTerms = PILFlavors.commercialRemix({
mintingFee: 0,
commercialRevShare: 5 * 10 ** 6, // 5% rev share
royaltyPolicy: ROYALTY_POLICY_LAP,
currencyToken: MERC20
});
```
## Test Your Code!
Run `forge build`. If everything is successful, the command should successfully compile.
Now run the test by executing the following command:
```bash theme={null}
forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/1_LicenseTerms.t.sol
```
## Attach Terms to Your IP
Congratulations, you created new license terms!
Follow the completed code all the way through.
Now that you have registered new license terms, we can attach them to an IP Asset. This will allow others to mint a license and use your IP, restricted by the terms.
We will go over this on the next page.
# Case Study: Registering a Derivative of Ippy
Source: https://docs.story.foundation/developers/tutorials/case-study-register-derivative-ippy
A Case Study showing how PiPi, a generative pfp project, registers derivatives of Story's official Ippy mascot.
[PiPi](https://pfp3.io/pipi/mint) is a free generative pfp project on Story that lets you mint derivative artworks of [Ippy](https://explorer.story.foundation/ipa/0xB1D831271A68Db5c18c8F0B69327446f7C8D0A42), Story's official mascot. Ippy has [Non-Commercial Social Remixing (NCSR)](/concepts/programmable-ip-license/pil-flavors#non-commercial-social-remixing) terms attached, which means anyone can use it or create derivative works as long as it's not used commercially and proper attribution is shown.
View the original Ippy mascot on our explorer.
View a derviative PiPi on our explorer.
View the PiPi contract source code.
When a PiPi is linked as a derivative of Ippy, it automatically inherits the same license terms (NCSR) and is linked in its ancestry graph, which you can see directly on our explorer:
In the following tutorial, you will learn how exactly these PiPi images were properly registered as derivatives of the official Ippy IP.
## Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup)
## 1. Setup Metadata
Before we register our new PiPi IP, we need to set up its metadata. There are two types of metadata:
1. NFT Metadata
2. IP Metadata
Learn how to properly set up NFT and IP Metadata.
Using [this PiPi](https://explorer.story.foundation/ipa/0xBB42BF2713ee736284C45B1b549a03625cc97e51) as an example, here is what the NFT & IP metadata should be:
```json NFT Metadata theme={null}
{
"name": "PiPi NFT #1103",
"image": "https://ipfs.io/ipfs/bafybeigsv4cgacndijwy6b7qhxbseonrybrcpbh47zrlm64gsjm4mlpb2q/nft_1103.jpeg",
"attributes": [
{
"trait_type": "Bg",
"value": "Orange"
},
{
"trait_type": "Body",
"value": "Pink"
},
{
"trait_type": "Eyes",
"value": "Cute"
},
{
"trait_type": "Cloth",
"value": "Blue"
},
{
"trait_type": "Glasses",
"value": "Neo"
},
{
"trait_type": "Hat",
"value": "Duck"
}
],
"description": "Pipi - The first Derivative IP Asset NFT collection on Story Protocol. Limited 2222 generative PFPs inspired by the Ippy, official Story mascot."
}
```
```json IP Metadata theme={null}
{
"title": "PiPi NFT",
"description": "Pipi - The first Derivative IP Asset NFT collection on Story Protocol. Limited 2222 generative PFPs inspired by the Ippy, official Story mascot.",
"image": "https://ipfs.io/ipfs/bafybeigsv4cgacndijwy6b7qhxbseonrybrcpbh47zrlm64gsjm4mlpb2q/nft_1103.jpeg",
"imageHash": "0xb930f3ba19350bddbcd8c180a3127086f6e454d29cd5b3db613c70bae2848329",
"mediaUrl": "https://ipfs.io/ipfs/bafybeigsv4cgacndijwy6b7qhxbseonrybrcpbh47zrlm64gsjm4mlpb2q/nft_1103.jpeg",
"mediaHash": "0xb930f3ba19350bddbcd8c180a3127086f6e454d29cd5b3db613c70bae2848329",
"mediaType": "image/jpeg",
"creators": [
{
"name": "PFP3",
"address": "0xF91510A17392Be6B3b6F620427051168A1e56A72",
"description": "PFP Generator",
"image": "https://utfs.io/f/XyGBmmuHQK18FodS0WDuqCo1LVerXR7sgm8vJnESazWcM5yB",
"socialMedia": [
{
"platform": "twitter",
"url": "https://x.com/pfp3_"
},
{
"platform": "website",
"url": "https://pfp3.io"
},
{
"platform": "discord",
"url": "https://discord.gg/pfp3"
}
],
"role": "creator",
"contributionPercent": 100
}
],
"tags": ["PiPi", "Derivative IPA", "NFT", "PF3", "PFP"],
"ipType": "NFT"
}
```
Once you have metadata written, you can upload them to IPFS and will later set it when minting our NFT.
## 2. Minting an NFT
When you want to register an IP on Story, you must first mint an NFT. This NFT represents the **ownership** over the [IP Asset](/concepts/ip-asset).
View the PiPi contract source code.
Here is part of the `_mintNFT` function in the `PiPi.sol` contract:
```sol PiPi.sol theme={null}
contract PiPi is ERC721, Ownable, IERC721Receiver {
// ... some code here ...
function whitelistMint() external payable returns (string memory, address) {
require(whitelistMintEnabled, "Whitelist mint is not active");
require(whitelist[msg.sender], "Address not whitelisted");
require(mintedCount[msg.sender] < WHITELIST_MAX_P_WALLET, "Whitelist mint limit reached");
require(_totalSupply < MAX_SUPPLY, "Max supply reached");
return _mintNFT(msg.sender);
}
function _mintNFT(address recipient) internal returns (string memory, address) {
uint256 newTokenId = _totalSupply + 1;
_safeMint(address(this), newTokenId);
address ipId = _registerAsIPAsset(newTokenId);
string memory nftUri = tokenURI(newTokenId);
bytes32 metadataHash = keccak256(abi.encodePacked(nftUri));
CORE_METADATA_MODULE.setAll(ipId, nftUri, metadataHash, metadataHash);
registerDerivativeForToken(ipId);
_safeTransfer(address(this), recipient, newTokenId, "");
// ... more code here ...
return (nftUri, ipId);
}
}
```
As you can see, the user calls `whitelistMint` which then calls `_mintNFT` after checking if the user is on a whitelist. On line 16, we are then minting a new NFT to the contract.
**Why do we mint an NFT to the contract and not the user?**
We later have to register the IP as a derivative of Ippy. Only the owner (the address holding the NFT) can register an IP as a derivative of another. So, we will mint the NFT to the contract => contract registers NFT as IP and then later as a derivative of Ippy => transfer NFT to the user.
## 3. Registering NFT as IP
Once we have minted a new NFT, we can register it as IP. On line 18 above, it calls a `_registerAsIPAsset` function:
```sol PiPi.sol theme={null}
function _registerAsIPAsset(uint256 tokenId) internal returns (address) {
try IP_ASSET_REGISTRY.register(block.chainid, address(this), tokenId) returns (address ipId) {
require(ipId != address(0), "IP Asset registration failed");
return ipId;
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("IP Asset registration failed");
}
}
```
All this is doing is calling the `register` function on the [IP Asset Registry](/concepts/registry/ip-asset-registry), which creates a new [IP Asset](/concepts/ip-asset) in our protocol, and returns an `ipId`.
## 4. Set Metadata on IP
Now that we have registered a new IP Asset, we can take our metadata from before and set it on the NFT & IP with the `CoreMetadataModule.sol`. As described [here](/concepts/ip-asset/overview#adding-nft-%26-ip-metadata-to-ip-asset), we need to set 4 params:
1. `nftMetadataHash`
2. `nftMetadataURI`
3. `ipMetadataHash`
4. `ipMetadataURI`
```sol PiPi.sol theme={null}
// handles the NFT's `nftMetadataHash`
// handles the IP's `ipMetadataURI` and `ipMetadataHash`
function _mintNFT(address recipient) internal returns (string memory, address) {
// ... some code here ...
string memory nftUri = tokenURI(newTokenId);
bytes32 metadataHash = keccak256(abi.encodePacked(nftUri));
CORE_METADATA_MODULE.setAll(ipId, nftUri, metadataHash, metadataHash);
// ... some code here ...
}
// handles the NFT's `nftMetadataURI`
function tokenURI(uint256 tokenId) public view override returns (string memory) {
return string(abi.encodePacked(_baseUri, StringUtils.uint2str(tokenId), ".json"));
}
```
## 5. Register as Derivative
Now that we have minted an NFT, registered it as IP, and set proper metadata, we can register it as a derivative of Ippy. The `PiPi.sol` contract uses `registerDerivativeForToken` to handle this:
```sol PiPi.sol theme={null}
function registerDerivativeForToken(address ipId) internal {
address[] memory parentIpIds = new address[](1);
parentIpIds[0] = 0xB1D831271A68Db5c18c8F0B69327446f7C8D0A42;
uint256[] memory licenseTermsIds = new uint256[](1);
licenseTermsIds[0] = 1;
address licenseTemplate = 0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316;
bytes memory royaltyContext = hex"0000000000000000000000000000000000000000";
uint256 maxMintingFee = 0;
uint32 maxRts = 0;
uint32 maxRevenueShare = 0;
LICENSING_MODULE.registerDerivative(
ipId,
parentIpIds,
licenseTermsIds,
licenseTemplate,
royaltyContext,
maxMintingFee,
maxRts,
maxRevenueShare
);
}
```
This function calls `registerDerivative` in the [Licensing Module](/concepts/licensing-module), with:
* `ipId`: the new `ipId` we got in step 3
* `parentIpIds`: an array that contains Ippy's `ipId`, which is `0xB1D831271A68Db5c18c8F0B69327446f7C8D0A42`
* `licenseTermsIds`: an array containing `1`, which is the license term ID of [Non-Commercial Social Remixing (NCSR)](/concepts/programmable-ip-license/pil-flavors#non-commercial-social-remixing). This means the derivative can use Ippy for free but not commercialize it
* `licenseTemplate`: the address of `PILicenseTemplate`, found in [Deployed Smart Contracts](/developers/deployed-smart-contracts)
* `royaltyContext`: just set to zero address
* `maxMintingFee`, `maxRts`, and `maxRevenueShare` can be set to 0. They don't do anything because the license terms are non-commercial.
## 6. Transfer NFT
Now that the contract has handled registering the IP as a derivative, it transfers the NFT to the user to have ownership over the PiPi IP:
```sol PiPi.sol theme={null}
function _mintNFT(address recipient) internal returns (string memory, address) {
// ... some code here ...
_safeTransfer(address(this), recipient, newTokenId, "");
// ... some code here ...
}
```
## 7. Done!
Congratulations, you registered a derivative of the official Ippy IP!
View a derviative PiPi on our explorer.
Explore more tutorials in our documentation
# Cross-Chain License Minting
Source: https://docs.story.foundation/developers/tutorials/cross-chain-license-mint
A guide on how to set up cross-chain license minting using deBridge.
In this tutorial, we will explore how to use [deBridge](https://docs.debridge.finance/) to perform cross-chain license minting. For this tutorial specifically, we'll be using Base \$ETH to mint a license on Story.
A working code example of minting a license on Story using Base \$ETH.
A working code example of minting a license on Story using Abstract \$ETH.
From a high level, it involves:
1. Constructing a deBridge API call that will return tx data to swap tokens across chains and perform some action (e.g. mint a license on Story)
2. Executing the API call to receive that tx data
3. Executing the transaction (using the returned tx data) on the source chain
## Step 1: Constructing the deBridge API Call
The first step is to construct a deBridge API call. The purpose of this API call is to receive back a response that will contain transaction data so we can then execute it on the source chain.
This deBridge order swaps tokens from one chain to another. We can also optionally attach a `dlnHook` that will execute an arbitrary action upon order completion (ex. after \$ETH has been swapped for \$IP). This is where the magic happens.
You can learn more about dlnHooks [here](https://docs.debridge.finance/dln-the-debridge-liquidity-network-protocol/integration-guidelines/interacting-with-the-api).
In this case, the `dlnHook` will be a call to `mintLicenseTokensCrossChain`, which is a function [in this contract](https://www.storyscan.io/address/0x6429A616F76a8958e918145d64bf7681C3936D6A?tab=contract) that wraps the received \$IP to \$WIP and then mints a license token on Story.
You can see that `mintLicenseTokensCrossChain` looks like this:
```solidity DebridgeLicenseTokenMinter.sol expandable theme={null}
function mintLicenseTokensCrossChain(
address licensorIpId,
uint256 licenseTermsId,
uint256 tokenAmount,
address receiver
) external payable {
uint256 amount = msg.value;
require(amount > 0, "DebridgeLicenseTokenMinter: zero amount");
// Create multicall data - using memory array directly to avoid stack issues
IMulticall.Call3Value[] memory calls = new IMulticall.Call3Value[](2);
// First call: deposit IP to get WIP tokens
calls[0] = IMulticall.Call3Value({
target: WIP,
allowFailure: false,
value: amount,
callData: abi.encodeWithSelector(IWrappedIP.deposit.selector)
});
// Second call: mint license tokens
calls[1] = IMulticall.Call3Value({
target: LICENSING_MODULE,
allowFailure: false,
value: 0,
callData: abi.encodeWithSelector(
ILicensingModule.mintLicenseTokens.selector,
licensorIpId,
PIL_TEMPLATE,
licenseTermsId,
tokenAmount,
receiver,
"",
0,
100_000_000
)
});
// Execute multicall and emit event
IMulticall.Result[] memory returnData = IMulticall(MULTICALL).aggregate3Value{ value: amount }(calls);
bytes memory raw = returnData[1].returnData;
uint256 startLicenseTokenId = abi.decode(raw, (uint256)); // if mintLicenseTokens returns uint256
emit LicenseTokensMinted(licensorIpId, receiver, startLicenseTokenId, tokenAmount);
}
```
You may be wondering, *"where does it `approve` the Royalty Module (what pays for the license minting fee) to spend the \$WIP?"* This is already done for you, since the Royalty Module is already approved to spend on behalf of Multicall.
The reason we had to make a Multicall contract in the first place, instead of simply bridging directly to \$WIP and calling `mintLicenseTokens` in the Licensing Module, is because the Royalty Module wouldn't be approved to spend the \$WIP. So we utilize the Multicall contract to take care of that for us instead.
To summarize, we will construct a deBridge API call that says *"we want to swap \$ETH for \$IP, then use a dlnHook to call a smart contract on Story that wraps \$IP to \$WIP and mints a license on Story".*
### Step 1a. Constructing the `dlnHook`
The `dlnHook` is a JSON object that will be attached to the deBridge API call. It will contain the following information:
* The type of action to execute (`evm_transaction_call`)
* The address of the contract to call (`DebridgeLicenseTokenMinter.sol`)
* The calldata to execute (`mintLicenseTokensCrossChain`)
```typescript main.ts theme={null}
const STORY_WRAP_THEN_LICENSE_MULTICALL =
"0x6429a616f76a8958e918145d64bf7681c3936d6a";
// Build the dlnHook for Story royalty payment
const buildRoyaltyPaymentHook = ({ ipId: `0x${string}`, licenseTermsId: bigint, receiverAddress: `0x${string}` }): string => {
// Encode the mintLicenseTokensCrossChain function call
const calldata = encodeFunctionData({
abi: [
{
name: "mintLicenseTokensCrossChain",
type: "function",
inputs: [
{ name: "licensorIpId", type: "address" },
{ name: "licenseTermsId", type: "uint256" },
{ name: "tokenAmount", type: "uint256" },
{ name: "receiver", type: "address" },
],
},
],
functionName: "mintLicenseTokensCrossChain",
args: [
ipId,
licenseTermsId,
BigInt(1),
receiverAddress,
],
});
// Build the dlnHook JSON
const dlnHook = {
type: "evm_transaction_call",
data: {
to: STORY_WRAP_THEN_LICENSE_MULTICALL,
calldata: calldata,
gas: 0,
},
};
return JSON.stringify(dlnHook);
};
```
### Step 1b. Constructing the deBridge API Call
Now that we have the `dlnHook`, we can construct the whole deBridge API call, including the `dlnHook`.
You can view deBridge's documentation on the `create-tx` endpoint [here](https://docs.debridge.finance/dln-the-debridge-liquidity-network-protocol/integration-guidelines/interacting-with-the-api/creating-an-order). I also highly recommend checking out the [Swagger UI](https://dln.debridge.finance/v1.0#/DLN/DlnOrderControllerV10_createOrder) for the `create-tx` endpoint as well.
| Attribute | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `srcChainId` | The ID of the source blockchain (e.g., Ethereum mainnet is 1). |
| `srcChainTokenIn` | The address of the token being swapped on the source chain (ETH in this case). |
| `srcChainTokenInAmount` | The amount of the source token to swap, set to `auto` for automatic calculation. |
| `dstChainId` | The ID of the destination blockchain (e.g., Story mainnet is 100000013). |
| `dstChainTokenOut` | The address of the token to receive on the destination chain (WIP token). |
| `dstChainTokenOutAmount` | The amount of the destination token to receive. It should be the same as the amount we're paying in `payRoyaltyOnBehalf` in step 1a. |
| `dstChainTokenOutRecipient` | This can just be the same as `senderAddress`. |
| `senderAddress` | The address initiating the transaction. |
| `srcChainOrderAuthorityAddress` | The address authorized to manage the order on the source chain. This can just be the same as `senderAddress`. |
| `dstChainOrderAuthorityAddress` | The address authorized to manage the order on the destination chain. This can just be the same as `senderAddress`. |
| `enableEstimate` | A flag to enable transaction simulation and estimation. |
| `prependOperatingExpenses` | A flag to include operating expenses in the transaction. |
| `dlnHook` | The URL-encoded hook that specifies additional actions to execute post-swap. |
```typescript main.ts theme={null}
import { base } from "viem/chains";
// ... previous code here ...
// Build deBridge API URL for cross-chain royalty payment
const buildDeBridgeApiUrl = ({
ipId: `0x${string}`,
licenseTermsId: bigint,
receiverAddress: `0x${string}`,
senderAddress: `0x${string}`,
paymentAmount: string // should be in wei
}): string => {
const dlnHook = buildRoyaltyPaymentHook({ ipId, licenseTermsId, receiverAddress });
const encodedHook = encodeURIComponent(dlnHook);
const url =
`https://dln.debridge.finance/v1.0/dln/order/create-tx?` +
`srcChainId=${base.id}` +
// we make this zero address which represents the native token ($ETH)
`&srcChainTokenIn=0x0000000000000000000000000000000000000000` +
// we set this to auto which will automatically calculate the amount of $ETH to swap
`&srcChainTokenInAmount=auto` +
// Story's mainnet chain ID
`&dstChainId=100000013` +
// we make this zero address which represents the native token ($IP)
`&dstChainTokenOut=0x0000000000000000000000000000000000000000` +
// we set the amount of $IP to pay for the license on Story
`&dstChainTokenOutAmount=${paymentAmount}` +
// the address of the contract that will wrap the $IP and mint the license on Story
`&dstChainTokenOutRecipient=${senderAddress}` +
// the address of the user initiating the transaction
`&senderAddress=${senderAddress}` +
// the address authorized to manage the order on the source chain
`&srcChainOrderAuthorityAddress=${senderAddress}` +
// the address authorized to manage the order on the destination chain
`&dstChainOrderAuthorityAddress=${senderAddress}` +
// we set this to true to enable transaction simulation and estimation
`&enableEstimate=true` +
// we set this to true to include operating expenses in the transaction
`&prependOperatingExpenses=true` +
`&dlnHook=${encodedHook}`;
return url;
};
```
## Step 2: Executing the API Call
Once the API call is constructed, execute it to receive a response. This response includes transaction data and an estimate for running the transaction on the source swap chain (e.g., Ethereum, Solana).
```typescript main.ts theme={null}
// ... previous code here ...
const getDeBridgeTransactionData = async ({
ipId: `0x${string}`,
licenseTermsId: bigint,
receiverAddress: `0x${string}`,
senderAddress: `0x${string}`,
paymentAmount: string // should be in wei
}): Promise => {
try {
const apiUrl = buildDeBridgeApiUrl({ ipId, licenseTermsId, receiverAddress, senderAddress, paymentAmount });
const response = await fetch(apiUrl, {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(
`deBridge API error: ${response.status} ${response.statusText}`
);
}
const data = (await response.json()) as DeBridgeApiResponse;
// Validate the response
if (!data.tx || !data.estimation || !data.orderId) {
throw new Error("Invalid deBridge API response: missing required fields");
}
return data;
} catch (error) {
console.error("Error calling deBridge API:", error);
throw error;
}
};
```
```typescript DeBridgeApiResponse theme={null}
export interface DeBridgeApiResponse {
estimation: {
srcChainTokenIn: {
amount: string;
approximateOperatingExpense: string;
};
dstChainTokenOut: {
amount: string;
maxTheoreticalAmount: string;
};
};
tx: {
to: string;
data: string;
value: string;
};
orderId: string;
}
```
## Step 3: Executing the Transaction on the Source Chain
Next, you would take the API response and execute the transaction on the source chain.
View [the docs here](https://docs.debridge.finance/dln-the-debridge-liquidity-network-protocol/integration-guidelines/interacting-with-the-api/submitting-an-order-creation-transaction) on submitting the transaction, including how this would be done differently on Solana.
```typescript TypeScript theme={null}
import { base } from "viem/chains";
import { createWalletClient, http, WalletClient, parseEther } from "viem";
import { privateKeyToAccount, Address, Account } from "viem/accounts";
import dotenv from "dotenv";
dotenv.config();
// Validate environment variables
if (!process.env.WALLET_PRIVATE_KEY) {
throw new Error("WALLET_PRIVATE_KEY is required in .env file");
}
// Create account from private key
const account: Account = privateKeyToAccount(
`0x${process.env.WALLET_PRIVATE_KEY}` as Address
);
// Initialize the wallet client
const walletClient = createWalletClient({
chain: base,
transport: http("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"), // Use Infura or another Ethereum provider
account,
}) as WalletClient;
// ... previous code here ...
// Function to send a transaction
const executeLicenseMint = async (params: {
ipId: `0x${string}`;
licenseTermsId: bigint;
receiverAddress: `0x${string}`;
senderAddress: `0x${string}`;
paymentAmount: string; // should be in wei
}) => {
// Get transaction data from deBridge
const deBridgeResponse = await getDeBridgeTransactionData(params);
try {
// Execute the transaction using the user's wallet
const txHash = await walletClient.sendTransaction({
to: deBridgeResponse.tx.to as Address,
data: deBridgeResponse.tx.data as Address,
value: BigInt(deBridgeResponse.tx.value),
account: account as Account,
chain: base,
});
console.log("Transaction sent:", txHash);
// Wait for the transaction to be mined
const receipt = await walletClient.waitForTransactionReceipt(txHash);
console.log("Transaction mined:", receipt.transactionHash);
} catch (error) {
console.error("Error sending transaction:", error);
}
};
// Example usage with a mock API response
const params = {
ipId: "0xcb6B9CCae4108A103097B30cFc25e1E257D4b5Fe",
licenseTermsId: BigInt(27910),
receiverAddress: "0x01", // replace with the address that should receive the license on Story
senderAddress: account.address,
paymentAmount: parseEther("0.00001"), // 0.00001 $WIP since the license on Story costs 0.00001 $WIP
};
// Execute the function to send the transaction
executeLicenseMint(params);
```
## Conclusion
Congratulations! You have successfully set up cross-chain license minting using deBridge.
A working code example of minting a license on Story using Base \$ETH.
A working code example of minting a license on Story using Abstract \$ETH.
# Cross-Chain Royalty Payments
Source: https://docs.story.foundation/developers/tutorials/cross-chain-royalty-payments
A guide on how to set up cross-chain royalty payments using deBridge.
In this tutorial, we will explore how to use [deBridge](https://docs.debridge.finance/) to perform cross-chain royalty payments. For this tutorial specifically, we'll pay an IP Asset on Story using Base \$ETH.
From a high level, it involves:
1. Constructing a deBridge API call that will return tx data to swap tokens across chains and perform some action (e.g. pay royalty to an IP Asset on Story)
2. Executing the API call to receive that tx data
3. Executing the transaction (using the returned tx data) on the source chain
## Step 1: Constructing the deBridge API Call
The first step is to construct a deBridge API call. The purpose of this API call is to receive back a response that will contain transaction data so we can then execute it on the source chain.
This deBridge order swaps tokens from one chain to another. We can also optionally attach a `dlnHook` that will execute an arbitrary action upon order completion (ex. after \$ETH has been swapped for \$WIP). This is where the magic happens.
You can learn more about dlnHooks [here](https://docs.debridge.finance/dln-the-debridge-liquidity-network-protocol/integration-guidelines/interacting-with-the-api).
In this case, the `dlnHook` will be a call to `payRoyaltyOnBehalf`, which is a function [in this contract](https://www.storyscan.io/address/0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086?tab=contract) that pays royalties to an IP Asset on Story.
You may be wondering, *"where does the Royalty Module (the contract spending the tokens on Story) get approved to spend the \$WIP?"* The answer is that deBridge automatically handles token approvals for you using their `IExternalCallExecutor`, where it approves the target address to spend the tokens on the destination chain. In this case because the target address is the Royalty Module itself, token approval is handled for us without having to create a custom multicall contract like we do in the [cross-chain license minting](/developers/tutorials/cross-chain-license-mint) tutorial.
You can learn more about the `IExternalCallExecutor` [here](https://docs.debridge.finance/dln-the-debridge-liquidity-network-protocol/protocol-specs/hook-data/anatomy-of-a-hook-for-the-evm-based-chains#universal-hook).
To summarize, we will construct a deBridge API call that says *"we want to swap \$ETH for \$IP, then use a dlnHook to call a smart contract on Story that pays royalties to an IP Asset on Story".*
### Step 1a. Constructing the `dlnHook`
The `dlnHook` is a JSON object that will be attached to the deBridge API call. It will contain the following information:
* The type of action to execute (`evm_transaction_call`)
* The address of the contract to call (`RoyaltyModule.sol`)
* The calldata to execute (`payRoyaltyOnBehalf`)
```typescript main.ts theme={null}
const ROYALTY_MODULE =
"0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086";
// Build the dlnHook for Story royalty payment
const buildRoyaltyPaymentHook = ({ ipId: `0x${string}`, amount: bigint }): string => {
// Encode the payRoyaltyOnBehalf function call
const calldata = encodeFunctionData({
abi: [
{
name: "payRoyaltyOnBehalf",
type: "function",
inputs: [
{ name: "receiverIpId", type: "address" },
{ name: "payerIpId", type: "address" },
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
],
},
],
functionName: "payRoyaltyOnBehalf",
args: [
ipId,
zeroAddress, // because it's not coming from another IP Asset. It's coming from an external address.
WIP_TOKEN_ADDRESS,
amount,
],
});
// Build the dlnHook JSON
const dlnHook = {
type: "evm_transaction_call",
data: {
to: ROYALTY_MODULE,
calldata: calldata,
gas: 0,
},
};
return JSON.stringify(dlnHook);
};
```
### Step 1b. Constructing the deBridge API Call
Now that we have the `dlnHook`, we can construct the whole deBridge API call, including the `dlnHook`.
You can view deBridge's documentation on the `create-tx` endpoint [here](https://docs.debridge.finance/dln-the-debridge-liquidity-network-protocol/integration-guidelines/interacting-with-the-api/creating-an-order). I also highly recommend checking out the [Swagger UI](https://dln.debridge.finance/v1.0#/DLN/DlnOrderControllerV10_createOrder) for the `create-tx` endpoint as well.
| Attribute | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `srcChainId` | The ID of the source blockchain (e.g., Ethereum mainnet is 1). |
| `srcChainTokenIn` | The address of the token being swapped on the source chain (ETH in this case). |
| `srcChainTokenInAmount` | The amount of the source token to swap, set to `auto` for automatic calculation. |
| `dstChainId` | The ID of the destination blockchain (e.g., Story mainnet is 100000013). |
| `dstChainTokenOut` | The address of the token to receive on the destination chain (WIP token). |
| `dstChainTokenOutAmount` | The amount of the destination token to receive. It should be the same as the amount we're paying in `payRoyaltyOnBehalf` in step 1a. |
| `dstChainTokenOutRecipient` | This can just be the same as `senderAddress`. |
| `senderAddress` | The address initiating the transaction. |
| `srcChainOrderAuthorityAddress` | The address authorized to manage the order on the source chain. This can just be the same as `senderAddress`. |
| `dstChainOrderAuthorityAddress` | The address authorized to manage the order on the destination chain. This can just be the same as `senderAddress`. |
| `enableEstimate` | A flag to enable transaction simulation and estimation. |
| `prependOperatingExpenses` | A flag to include operating expenses in the transaction. |
| `dlnHook` | The URL-encoded hook that specifies additional actions to execute post-swap. |
```typescript main.ts theme={null}
import { base } from "viem/chains";
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
// ... previous code here ...
// Build deBridge API URL for cross-chain royalty payment
const buildDeBridgeApiUrl = ({
ipId: `0x${string}`,
senderAddress: `0x${string}`,
paymentAmount: string // should be in wei
}): string => {
const dlnHook = buildRoyaltyPaymentHook({ ipId, paymentAmount });
const encodedHook = encodeURIComponent(dlnHook);
const url =
`https://dln.debridge.finance/v1.0/dln/order/create-tx?` +
`srcChainId=${base.id}` +
// we make this zero address which represents the native token ($ETH)
`&srcChainTokenIn=0x0000000000000000000000000000000000000000` +
// we set this to auto which will automatically calculate the amount of $ETH to swap
`&srcChainTokenInAmount=auto` +
// Story's mainnet chain ID
`&dstChainId=100000013` +
// we make this zero address which represents the native token ($IP)
`&dstChainTokenOut=${WIP_TOKEN_ADDRESS}` +
// we set the amount of $IP to pay for the payment on Story
`&dstChainTokenOutAmount=${paymentAmount}` +
// the address of the contract that will pay the royalty on Story
`&dstChainTokenOutRecipient=${senderAddress}` +
// the address of the user initiating the transaction
`&senderAddress=${senderAddress}` +
// the address authorized to manage the order on the source chain
`&srcChainOrderAuthorityAddress=${senderAddress}` +
// the address authorized to manage the order on the destination chain
`&dstChainOrderAuthorityAddress=${senderAddress}` +
// we set this to true to enable transaction simulation and estimation
`&enableEstimate=true` +
// we set this to true to include operating expenses in the transaction
`&prependOperatingExpenses=true` +
`&dlnHook=${encodedHook}`;
return url;
};
```
## Step 2: Executing the API Call
Once the API call is constructed, execute it to receive a response. This response includes transaction data and an estimate for running the transaction on the source swap chain (e.g., Ethereum, Solana).
```typescript main.ts theme={null}
// ... previous code here ...
const getDeBridgeTransactionData = async ({
ipId: `0x${string}`,
senderAddress: `0x${string}`,
paymentAmount: string // should be in wei
}): Promise => {
try {
const apiUrl = buildDeBridgeApiUrl({ ipId, senderAddress, paymentAmount });
const response = await fetch(apiUrl, {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(
`deBridge API error: ${response.status} ${response.statusText}`
);
}
const data = (await response.json()) as DeBridgeApiResponse;
// Validate the response
if (!data.tx || !data.estimation || !data.orderId) {
throw new Error("Invalid deBridge API response: missing required fields");
}
return data;
} catch (error) {
console.error("Error calling deBridge API:", error);
throw error;
}
};
```
```typescript DeBridgeApiResponse theme={null}
export interface DeBridgeApiResponse {
estimation: {
srcChainTokenIn: {
amount: string;
approximateOperatingExpense: string;
};
dstChainTokenOut: {
amount: string;
maxTheoreticalAmount: string;
};
};
tx: {
to: string;
data: string;
value: string;
};
orderId: string;
}
```
## Step 3: Executing the Transaction on the Source Chain
Next, you would take the API response and execute the transaction on the source chain.
View [the docs here](https://docs.debridge.finance/dln-the-debridge-liquidity-network-protocol/integration-guidelines/interacting-with-the-api/submitting-an-order-creation-transaction) on submitting the transaction, including how this would be done differently on Solana.
```typescript TypeScript theme={null}
import { base } from "viem/chains";
import { createWalletClient, http, WalletClient, parseEther } from "viem";
import { privateKeyToAccount, Address, Account } from "viem/accounts";
import dotenv from "dotenv";
dotenv.config();
// Validate environment variables
if (!process.env.WALLET_PRIVATE_KEY) {
throw new Error("WALLET_PRIVATE_KEY is required in .env file");
}
// Create account from private key
const account: Account = privateKeyToAccount(
`0x${process.env.WALLET_PRIVATE_KEY}` as Address
);
// Initialize the wallet client
const walletClient = createWalletClient({
chain: base,
transport: http("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"), // Use Infura or another Ethereum provider
account,
}) as WalletClient;
// ... previous code here ...
// Function to send a transaction
const executeLicenseMint = async (params: {
ipId: `0x${string}`;
senderAddress: `0x${string}`;
paymentAmount: string; // should be in wei
}) => {
// Get transaction data from deBridge
const deBridgeResponse = await getDeBridgeTransactionData(params);
try {
// Execute the transaction using the user's wallet
const txHash = await walletClient.sendTransaction({
to: deBridgeResponse.tx.to as Address,
data: deBridgeResponse.tx.data as Address,
value: BigInt(deBridgeResponse.tx.value),
account: account as Account,
chain: base,
});
console.log("Transaction sent:", txHash);
// Wait for the transaction to be mined
const receipt = await walletClient.waitForTransactionReceipt(txHash);
console.log("Transaction mined:", receipt.transactionHash);
} catch (error) {
console.error("Error sending transaction:", error);
}
};
// Example usage with a mock API response
const params = {
ipId: "0xcb6B9CCae4108A103097B30cFc25e1E257D4b5Fe",
senderAddress: account.address,
paymentAmount: parseEther("5"), // 5 $WIP
};
// Execute the function to send the transaction
executeLicenseMint(params);
```
## Conclusion
Congratulations! You have successfully set up cross-chain royalty payments using deBridge.
# Easy $IP Onboarding
Source: https://docs.story.foundation/developers/tutorials/easy-ip-onboarding
An example of how to integrate purchasing $IP with Apple Pay, Venmo, Debit Card, Bank, and more with Halliday.
View the completed code for this tutorial.
This tutorial will show you how to integrate purchasing \$IP with Apple Pay, Venmo, Debit Card, Bank, and more into your DApp with Halliday.
**This tutorial is a React/Next.js tutorial**. It is also based on the [Halliday Docs](https://docs.halliday.xyz/pages/index-page).
Here is what the end result will look like:
## Instructions
In order to use Halliday, you will need to get an API key. Right now the process is to email [partnerships@halliday.xyz](mailto:partnerships@halliday.xyz). But this may change, so I recommend checking the [Halliday Docs](https://docs.halliday.xyz/pages/payments-hello-world) for the most up to date information.
Next, install the Halliday Payments SDK in the root folder of your project.
```bash npm theme={null}
npm install @halliday-sdk/payments
```
```bash yarn theme={null}
yarn add @halliday-sdk/payments
```
In your `.env` file, add your Halliday API key.
```env theme={null}
NEXT_PUBLIC_HALLIDAY_PUBLIC_API_KEY=your-api-key
```
Lastly, integrate Halliday Payments into your existing application with a few lines of code.
In this example, we make it so that the Halliday popup is embedded in a div with the id `halliday-embed`. Such that when the user loads the page, it is already there. However you can change this so that it pops up when the user clicks a button. Check out the [Halliday Docs](https://docs.halliday.xyz/pages/payments-widget) for more information.
```tsx theme={null}
"use client";
import { openHallidayPayments } from "@halliday-sdk/payments";
import { useEffect } from "react";
export default function Home() {
useEffect(() => {
openHallidayPayments({
apiKey: process.env.NEXT_PUBLIC_HALLIDAY_PUBLIC_API_KEY as string,
// $IP on Story
outputs: ["story:0x"],
// $USDC.e on Story
// outputs: ["story:0xf1815bd50389c46847f0bda824ec8da914045d14"],
sandbox: false,
windowType: "EMBED",
targetElementId: "halliday-embed",
});
}, []);
return (
);
}
```
That's it! You can now use Halliday to purchase \$IP on Story.
View the completed code for this tutorial.
# Encode Function Data
Source: https://docs.story.foundation/developers/tutorials/encode-func-data
A short tutorial on how to encode Story smart contract function data for things like Crossmint.
Sometimes you want to encode smart contract function data for things like [sending arbitrary transactions via Crossmint](https://docs.crossmint.com/solutions/story-protocol/wallets/server-side-wallets#send-arbitrary-transaction).
As seen [in Crossmint's docs](https://docs.crossmint.com/solutions/story-protocol/wallets/server-side-wallets#send-arbitrary-transaction), you can get encoded function data via Story's SDK. However this is shortly going to be deprecated and it does not support every function anyway.
So here's a quick tutorial on how to get encoded function data for any Story smart contract function.
View the completed code for this tutorial.
## Step 1: Get the function data
As an example, let's try to call the [mintAndRegisterIpAndAttachPilTerms](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/workflows/LicenseAttachmentWorkflows.sol#L130) in our periphery contract. This function mints & registers a new IP and attaches [PIL Terms](/concepts/programmable-ip-license/pil-terms) to it.
As shown in [Deployed Smart Contracts](/developers/deployed-smart-contracts), `LicenseAttachmentWorkflows` is deployed at `0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8` on Aeneid (at the time of writing this tutorial).
There's a few things we'll need:
1. Contract ABI - you can get this by going to [our block explorer and looking up the contract](https://aeneid.storyscan.io/address/0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8). This is a proxy contract, so go to Contract > Read/Write proxy > Click on the implementation address > Contract > ABI. After all that, you should end up [here](https://aeneid.storyscan.io/address/0x887c22833bf7F8E0E19F7d994fec964A82c030FB?tab=contract_abi).
2. Function Args - you can get these just by looking at the [code](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/workflows/LicenseAttachmentWorkflows.sol#L130).
Great! Now let's build our transaction request.
```typescript theme={null}
import { encodeFunctionData } from "viem";
import { Account, Address, privateKeyToAccount } from "viem/accounts";
const account: Account = privateKeyToAccount(
`0x${process.env.WALLET_PRIVATE_KEY}` as Address
);
const transactionRequest = {
to: "0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8" as `0x${string}`,
data: encodeFunctionData({
abi: CONTRACT_ABI, // the abi you copied above
functionName: "mintAndRegisterIpAndAttachPILTerms",
args: [
"0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc", // example spg nft contract I use on Aeneid
account.address,
// example metadata values
{
ipMetadataURI:
"https://ipfs.io/ipfs/bafkreiabrkevameeffdpjpizchywldogzai7kp5oodawbhgbojyeomk7uq",
ipMetadataHash:
"0x018a895030842946f4bd1911f1658dc6c811f53fae70c1609cc1727047315fa4",
nftMetadataURI:
"https://ipfs.io/ipfs/bafkreicbuti233kvewqs7uwb5y2toexj5gafgvsr5mqmnnx7cuof53gvsa",
nftMetadataHash:
"0x41a4d1aded5525a12fd2c1ee353712e9e980535651eb20c6b6ff151c5eecd590",
},
// example PIL terms
[
{
terms: {
transferable: true,
royaltyPolicy: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E",
defaultMintingFee: 0n,
expiration: 0n,
commercialUse: true,
commercialAttribution: true,
commercializerChecker: "0x0000000000000000000000000000000000000000",
commercializerCheckerData: "0x",
commercialRevShare: 0,
commercialRevCeiling: 0n,
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: 0n,
currency: "0x1514000000000000000000000000000000000000",
uri: "https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json",
},
licensingConfig: {
mintingFee: 0n,
isSet: false,
disabled: false,
commercialRevShare: 0,
expectGroupRewardPool: "0x0000000000000000000000000000000000000000",
expectMinimumGroupRewardShare: 0,
licensingHook: "0x0000000000000000000000000000000000000000",
hookData: "0x",
},
},
],
true,
],
}),
};
```
## Step 2: Send the transaction
At this point, we have the `data` and `to` needed for sending this transaction, for example if we were using [Crossmint](https://docs.crossmint.com/solutions/story-protocol/wallets/server-side-wallets#send-arbitrary-transaction).
However we could also then send this transaction ourselves:
```typescript theme={null}
import { aeneid } from "@story-protocol/core-sdk";
import { createWalletClient, http, WalletClient } from "viem";
// other code here
const walletClient = createWalletClient({
chain: aeneid,
transport: http("https://aeneid.storyrpc.io"),
account,
}) as WalletClient;
const txHash = await walletClient.sendTransaction({
...transactionRequest,
account,
chain: aeneid,
});
console.log(`Transaction sent: https://aeneid.storyscan.io/tx/${txHash}`);
```
## Done!
View the completed code for this tutorial.
Explore more tutorials in our documentation
# Finetune Images on Story
Source: https://docs.story.foundation/developers/tutorials/finetune-images
Learn how to use the FLUX Finetuning API to finetune images and then register the output on Story in TypeScript.
View the completed code for this tutorial.
In this tutorial, you will use the FLUX Finetuning API to take a bunch of images of Story's mascot "Ippy" and finetune an AI model to create similar images along with a prompt. Then you will monetize and protect the output IP on Story.
**This Tutorial is in TypeScript**
Steps 1-3 of this tutorial are based on the [FLUX Finetuning Beta Guide](https://docs.bfl.ml/finetuning/), which contains examples for calling their API in Python, however I have rewritten them in TypeScript.
## The Explanation
Generative text-to-image models often do not fully capture a creator's unique vision, and have insufficient knowledge about specific objects, brands or visual styles. With the FLUX Pro Finetuning API, creators can use existing images to finetune an AI to create similar images, along with a prompt.
When an image is created, we will register it as IP on Story in order to grow, monetize, and protect the IP.
## 0. Before you Start
There are a few steps you have to complete before you can start the tutorial.
1. You will need to install [Node.js](https://nodejs.org/en/download) and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). If you've coded before, you likely have these.
2. Add your Story Network Testnet wallet's private key to `.env` file:
```yaml .env theme={null}
WALLET_PRIVATE_KEY=
```
3. Go to [Pinata](https://pinata.cloud/) and create a new API key. Add the JWT to your `.env` file:
```yaml .env theme={null}
PINATA_JWT=
```
4. Go to [BFL](https://api.us1.bfl.ai/auth/profile) and create a new API key. Add the new key to your `.env` file:
**BFL Credits**
In order to generate an image, you'll need BFL credits. If you just created an account, you will need to purchase credits [here](https://api.us1.bfl.ai/auth/profile).
You can also see the pricing for each of the API endpoints [here](https://docs.bfl.ml/pricing/).
```yaml .env theme={null}
BFL_API_KEY=
```
5. Add your preferred Story RPC URL to your `.env` file. You can just use the public default one we provide:
```yaml .env theme={null}
RPC_PROVIDER_URL=https://aeneid.storyrpc.io
```
6. Install the dependencies:
```bash Terminal theme={null}
npm install @story-protocol/core-sdk axios pinata-web3 viem
```
## 1. Compile the Training Data
In order to create a finetune, we'll need the input training data!
1. Create a folder in your project called `images`. In that folder, add a bunch of images that you want your finetune to train on. *Supported formats: JPG, JPEG, PNG, and WebP. Also recommended to use more than 5 images.*
2. Add Text Descriptions (Optional): In the same folder, create text files with descriptions for your images. Text files should share the same name as their corresponding images. *Example: if your image is "sample.jpg", create "sample.txt"*
3. Compress your folder into a ZIP file. It should be named `images.zip`
## 2. Create a Finetune
In order to generate an image using a similar style as input images, we need to create a **finetune**. Think of a finetune as an AI that knows all of your input images and can then start producing new ones.
Let's make a function that calls FLUX's `/v1/finetune` API route. Create a `flux` folder, and inside that folder add a file named `requestFinetuning.ts` and add the following code:
**Official Docs**
In order to learn what each of the parameters in the payload are, see the official `/v1/finetune` API docs [here](https://api.us1.bfl.ai/scalar#tag/tasks/POST/v1/finetune).
```typescript flux/requestFinetuning.ts theme={null}
import axios from "axios";
import fs from "fs";
interface FinetunePayload {
finetune_comment: string;
trigger_word: string;
file_data: string;
iterations: number;
mode: string;
learning_rate: number;
captioning: boolean;
priority: string;
lora_rank: number;
finetune_type: string;
}
export async function requestFinetuning(
zipPath: string,
finetuneComment: string,
triggerWord = "TOK",
mode = "general",
iterations = 300,
learningRate = 0.00001,
captioning = true,
priority = "quality",
finetuneType = "full",
loraRank = 32
) {
if (!fs.existsSync(zipPath)) {
throw new Error(`ZIP file not found at ${zipPath}`);
}
const modes = ["character", "product", "style", "general"];
if (!modes.includes(mode)) {
throw new Error(`Invalid mode. Must be one of: ${modes.join(", ")}`);
}
const fileData = fs.readFileSync(zipPath);
const encodedZip = Buffer.from(fileData).toString("base64");
const url = "https://api.us1.bfl.ai/v1/finetune";
const headers = {
"Content-Type": "application/json",
"X-Key": process.env.BFL_API_KEY,
};
const payload: FinetunePayload = {
finetune_comment: finetuneComment,
trigger_word: triggerWord,
file_data: encodedZip,
iterations,
mode,
learning_rate: learningRate,
captioning,
priority,
lora_rank: loraRank,
finetune_type: finetuneType,
};
try {
const response = await axios.post(url, payload, { headers });
return response.data;
} catch (error) {
throw new Error(`Finetune request failed: ${error}`);
}
}
```
Next, create a file named `train.ts` and call the `requestFinetuning` function we just made:
**Warning: This is expensive!**
Creating a new finetune is expensive, ranging from $2-$6 at the time of me writing this tutorial. Please review the "FLUX PRO FINETUNE: TRAINING" section on the [pricing page](https://docs.bfl.ml/pricing/).
```typescript train.ts theme={null}
import { requestFinetuning } from "./flux/requestFinetuning";
async function main() {
const response = await requestFinetuning("./images.zip", "ippy-finetune");
console.log(response);
}
main();
```
This will log something that looks like:
```json theme={null}
{
"finetune_id": "6fc5e628-6f56-48ec-93cb-c6a6b22bf5a"
}
```
This is your `finetune_id`, and will be used to create images in the following steps.
## 3. Wait for Finetune
Before we can generate images with our finetuned model, we have to wait for FLUX to finish training!
In our `flux` folder, create a file named `finetune-progress.ts` and add the following code:
**Official Docs**
In order to learn what each of the parameters in the payload are, see the official `/v1/get_result` API docs [here](https://api.us1.bfl.ai/scalar#tag/utility/GET/v1/get_result).
```typescript flux/finetuneProgress.ts theme={null}
import axios from "axios";
export async function finetuneProgress(finetuneId: string) {
const url = "https://api.us1.bfl.ai/v1/get_result";
const headers = {
"Content-Type": "application/json",
"X-Key": process.env.BFL_API_KEY,
};
try {
const response = await axios.get(url, {
headers,
params: { id: finetuneId },
});
return response.data;
} catch (error) {
throw new Error(`Finetune progress failed: ${error}`);
}
}
```
Next, create a file named `finetune-progress.ts` and call the `finetuneProgress` function we just made:
```typescript finetune-progress.ts theme={null}
import { finetuneProgress } from "./flux/finetuneProgress";
// input your finetune_id here
const FINETUNE_ID = "";
async function main() {
const progress = await finetuneProgress(FINETUNE_ID);
console.log(progress);
}
main();
```
This will log something that looks like:
```json theme={null}
{
"id": "023a1507-369e-46e0-bd6d-1f3446d7d5f2",
"status": "Pending",
"result": null,
"progress": null
}
```
As you can see, the status is still pending. We must wait until the training is 'Ready' before we can move on to the next step.
## 4. Run Inference
**Warning: This costs money.**
Although very cheap, running an inference does cost money, ranging from \$0.06-0.07 at the time of me writing this tutorial. Please review the "FLUX PRO FINETUNE: INFERENCE" section on the [pricing page](https://docs.bfl.ml/pricing/).
Now that we have trained a finetune, we will use the model to create images. "Running an inference" simply means using our new model (identified by its `finetune_id`), which is trained on our images, to create new images.
There are several different inference endpoints we can use, each with [their own pricing](https://docs.bfl.ml/pricing/) (found at the bottom of the page). For this tutorial, I'll be using the `/v1/flux-pro-1.1-ultra-finetuned` endpoint, which is documented [here](https://api.us1.bfl.ai/scalar#tag/tasks/POST/v1/flux-pro-1.1-ultra-finetuned).
In our `flux` folder, create a `finetuneInference.ts` file and add the following code:
**Official Docs**
In order to learn what each of the parameters in the payload are, see the official `/v1/flux-pro-1.1-ultra-finetuned` API docs [here](https://api.us1.bfl.ai/scalar#tag/tasks/POST/v1/flux-pro-1.1-ultra-finetuned).
```typescript flux/finetineInference.ts theme={null}
import axios from "axios";
export async function finetuneInference(
finetuneId: string,
prompt: string,
finetuneStrength = 1.2,
endpoint = "flux-pro-1.1-ultra-finetuned",
additionalParams: Record = {}
) {
const url = `https://api.us1.bfl.ai/v1/${endpoint}`;
const headers = {
"Content-Type": "application/json",
"X-Key": process.env.BFL_API_KEY,
};
const payload = {
finetune_id: finetuneId,
finetune_strength: finetuneStrength,
prompt,
...additionalParams,
};
try {
const response = await axios.post(url, payload, { headers });
return response.data;
} catch (error) {
throw new Error(`Finetune inference failed: ${error}`);
}
}
```
Next, create a file named `inference.ts` and call the `finetuneInference` function we just made. The first parameter should be the `finetune_id` we got from running the script above, and the second parameter is a prompt to generate a new image.
```typescript inference.ts theme={null}
import { finetuneInference } from "./flux/finetuneInference";
// input your finetune_id here
const FINETUNE_ID = "";
// add your prompt here
const PROMPT = "A picture of Ippy being really happy.";
async function main() {
const inference = await finetuneInference(FINETUNE_ID, PROMPT);
console.log(inference);
}
main();
```
This will log something that looks like:
```json theme={null}
{
"id": "023a1507-369e-46e0-bd6d-1f3446d7d5f2",
"status": "Pending",
"result": null,
"progress": null
}
```
As you can see, the status is still pending. We must wait until the generation is ready to view our image. To do this, we will need a function to fetch our new inference to see if its ready and view the details about it.
In our `flux` folder, create a file named `getInference.ts` and add the following code:
**Official Docs**
In order to learn what each of the parameters in the payload are, see the official `/v1/get_result` API docs [here](https://api.us1.bfl.ai/scalar#tag/utility/GET/v1/get_result).
```typescript flux/getInference.ts theme={null}
import axios from "axios";
export async function getInference(id: string) {
const url = "https://api.us1.bfl.ai/v1/get_result";
const headers = {
"Content-Type": "application/json",
"X-Key": process.env.BFL_API_KEY,
};
try {
const response = await axios.get(url, { headers, params: { id } });
return response.data;
} catch (error) {
throw new Error(`Inference retrieval failed: ${error}`);
}
}
```
Back in our `inference.ts` file, lets add a loop that continuously fetches the inference until it's ready. When it's ready, we will view the new image.
```typescript inference.ts theme={null}
import { finetuneInference } from "./flux/finetuneInference";
import { getInference } from "./flux/getInference";
// input your finetune_id here
const FINETUNE_ID = "";
// add your prompt here
const PROMPT = "A picture of Ippy being really happy.";
async function main() {
const inference = await finetuneInference(FINETUNE_ID, PROMPT);
let inferenceData = await getInference(inference.id);
while (inferenceData.status != "Ready") {
console.log("Waiting for inference to complete...");
// wait 5 seconds
await new Promise((resolve) => setTimeout(resolve, 5000));
// fetch the inference again
inferenceData = await getInference(inference.id);
}
// now the inference data is ready
console.log(inferenceData);
}
main();
```
Once the loop completed, the final log will look like:
```json theme={null}
{
"id": "023a1507-369e-46e0-bd6d-1f3446d7d5f2",
"status": "Ready",
"result": {
"sample": "https://delivery-us1.bfl.ai/results/746585f8d1b341f3a8735ababa563ac1/sample.jpeg?se=2025-01-16T19%3A50%3A11Z&sp=r&sv=2024-11-04&sr=b&rsct=image/jpeg&sig=pPtWnntLqc49hfNnGPgTf4BzS6MZcBgHayrYkKe%2BZIc%3D",
"prompt": "A picture of Ippy being really happy."
},
"progress": null
}
```
You can paste the `sample` into your browser and see the final result! Make sure to save this image as it will disappear eventually.
## 5. Set up your Story Config
Next we will register this image on Story as an [IP Asset](/concepts/ip-asset) in order to monetize and license the IP. Create a `story` folder and add a `utils.ts` file. In there, add the following code to set up your Story Config:
Associated docs: [TypeScript SDK Setup](/developers/typescript-sdk/setup)
```typescript story/utils.ts theme={null}
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
import { http } from "viem";
import { privateKeyToAccount, Address, Account } from "viem/accounts";
const privateKey: Address = `0x${process.env.WALLET_PRIVATE_KEY}`;
export const account: Account = privateKeyToAccount(privateKey);
const config: StoryConfig = {
account: account,
transport: http(process.env.RPC_PROVIDER_URL),
chainId: "aeneid",
};
export const client = StoryClient.newClient(config);
```
## 6. Upload Inference to IPFS
Now that we have made a new inference, we'll have to store the image `sample` file ourselves on IPFS because the sample is only temporary.
In a new `pinata` folder, create a `uploadToIpfs.ts` file and create a function to upload our image and get details about it:
```typescript pinata/uploadToIpfs.ts theme={null}
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
});
export async function uploadImageAndGetDetails(
imageUrl: string
): Promise<{ ipfsCid: string; contentType: string; contentHash: string }> {
try {
const response = await axios.get(imageUrl, {
responseType: "arraybuffer",
validateStatus: (status) => status === 200,
});
const contentType = response.headers["content-type"];
if (!contentType?.startsWith("image/")) {
throw new Error("URL does not point to an image");
}
const extension = contentType.split("/")[1];
const filename = `${Date.now()}-${Math.random()
.toString(36)
.slice(2)}.${extension}`;
const buffer = Buffer.from(response.data);
const contentHash =
"0x" + createHash("sha256").update(buffer).digest("hex");
const file = new File([buffer], filename, { type: contentType });
const { IpfsHash } = await pinata.upload.file(file);
return { ipfsCid: IpfsHash, contentType, contentHash };
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Failed to fetch image: ${error.message}`);
}
throw error;
}
}
```
We will now use this function in the following step.
## 7. Set up your IP Metadata
In your `story` folder, create a `registerIp.ts` file.
View the [IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard) and construct the metadata for your IP as shown below:
```typescript story/registerIp.ts theme={null}
import { client, account } from "./utils";
import { uploadImageAndGetDetails } from "../pinata/uploadToIpfs";
export async function registerIp(inference) {
const { ipfsCid, contentType, contentHash } = await uploadImageAndGetDetails(
inference.result.sample
);
// Docs: https://docs.story.foundation/concepts/ip-asset/ipa-metadata-standard
const ipMetadata = {
title: "Happy Ippy",
description:
"An image of Ippy being really happy, generated by FLUX's 1.1 [pro] ultra Finetune",
image: `https://ipfs.io/ipfs/${ipfsCid}`,
imageHash: contentHash,
mediaUrl: `https://ipfs.io/ipfs/${ipfsCid}`,
mediaHash: contentHash,
mediaType: contentType,
creators: [
{
name: "Jacob Tucker",
contributionPercent: 100,
address: account.address,
},
],
};
}
```
## 8. Set up your NFT Metadata
In the `registerIp.ts` file, configure your NFT Metadata, which follows the [OpenSea ERC-721 Standard](https://docs.opensea.io/docs/metadata-standards).
```typescript story/registerIp.ts theme={null}
import { client, account } from "./utils";
import { uploadImageAndGetDetails } from "../pinata/uploadToIpfs";
export async function registerIp(inference) {
// previous code here...
// Docs: https://docs.opensea.io/docs/metadata-standards
const nftMetadata = {
name: "Ippy Ownership NFT",
description:
"This NFT represents ownership of the Happy Ippy image generated by FLUX's 1.1 [pro] ultra Finetune",
image: `https://ipfs.io/ipfs/${ipfsCid}`,
attributes: [
{
key: "Model",
value: "FLUX 1.1 [pro] ultra Finetune",
},
{
key: "Prompt",
value: "A picture of Ippy being really happy.",
},
],
};
}
```
## 9. Upload your IP and NFT Metadata to IPFS
In the `pinata` folder, create a function to upload your IP & NFT Metadata objects to IPFS:
```typescript pinata/uploadToIpfs.ts theme={null}
// previous code here ...
export async function uploadJSONToIPFS(jsonMetadata: any): Promise {
const { IpfsHash } = await pinata.upload.json(jsonMetadata);
return IpfsHash;
}
```
You can then use that function to upload your metadata, as shown below:
```typescript story/registerIp.ts theme={null}
import { client, account } from "./utils";
import {
uploadImageAndGetDetails,
uploadJSONToIPFS,
} from "../pinata/uploadToIpfs";
import { createHash } from "crypto";
export async function registerIp(inference) {
// previous code here...
const ipIpfsHash = await uploadJSONToIPFS(ipMetadata);
const ipHash = createHash("sha256")
.update(JSON.stringify(ipMetadata))
.digest("hex");
const nftIpfsHash = await uploadJSONToIPFS(nftMetadata);
const nftHash = createHash("sha256")
.update(JSON.stringify(nftMetadata))
.digest("hex");
}
```
## 10. Register the NFT as an IP Asset
Next we will mint an NFT, register it as an [IP Asset](/concepts/ip-asset), set [License Terms](/concepts/licensing-module/license-terms) on the IP, and then set both NFT & IP metadata.
Luckily, we can use the `registerIpAsset` function to mint an NFT and register it as an IP Asset in the same transaction.
This function needs an SPG NFT Contract to mint from. For simplicity, you can use a public collection we have created for you on Aeneid testnet: `0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc`.
Using the public collection we provide for you is fine, but when you do this for real, you should make your own NFT Collection for your IPs. You can do this in 2 ways:
1. Deploy a contract that implements the [ISPGNFT](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/interfaces/ISPGNFT.sol) interface, or use the SDK's [createNFTCollection](/sdk-reference/nftclient#createnftcollection) function (shown below) to do it for you. This will give you your own SPG NFT Collection that only you can mint from.
```typescript createSpgNftCollection.ts theme={null}
import { zeroAddress } from "viem";
import { client } from "./utils";
async function createSpgNftCollection() {
const newCollection = await client.nftClient.createNFTCollection({
name: "Test NFTs",
symbol: "TEST",
isPublicMinting: false,
mintOpen: true,
mintFeeRecipient: zeroAddress,
contractURI: "",
});
console.log(
`New SPG NFT collection created at transaction hash ${newCollection.txHash}`
);
console.log(`NFT contract address: ${newCollection.spgNftContract}`);
}
createSpgNftCollection();
```
2. Create a custom ERC-721 NFT collection on your own. See a working code example [here](https://github.com/storyprotocol/typescript-tutorial/blob/main/scripts/registration/registerCustom.ts). This is helpful if you **already have a custom NFT contract that has your own custom logic, or if your IPs themselves are NFTs.**
Associated Docs:
[ipAsset.registerIpAsset](/sdk-reference/ipasset#registeripasset)
```typescript story/registerIp.ts theme={null}
import { client, account } from "./utils";
import {
uploadImageAndGetDetails,
uploadJSONToIPFS,
} from "../pinata/uploadToIpfs";
import { createHash } from "crypto";
import { Address } from "viem";
export async function registerIp(inference) {
// previous code here ...
const response = await client.ipAsset.registerIpAsset({
nft: {
type: "mint",
spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e7839FAbc",
},
ipMetadata: {
ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`,
ipMetadataHash: `0x${ipHash}`,
nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`,
nftMetadataHash: `0x${nftHash}`,
},
});
console.log(
`Root IPA created at transaction hash ${response.txHash}, IPA ID: ${response.ipId}`
);
console.log(
`View on the explorer: https://aeneid.explorer.story.foundation/ipa/${response.ipId}`
);
}
```
## 11. Register our Inference
Now that we have completed our `registerIp` function, let's add it to our `inference.ts` file:
```typescript inference.ts theme={null}
import { finetuneInference } from "./flux/finetuneInference";
import { getInference } from "./flux/getInference";
import { registerIp } from "./story/registerIp";
const FINETUNE_ID = "";
const PROMPT = "A picture of Ippy being really happy.";
async function main() {
const inference = await finetuneInference(FINETUNE_ID, PROMPT);
let inferenceData = await getInference(inference.id);
while (inferenceData.status != "Ready") {
console.log("Waiting for inference to complete...");
await new Promise((resolve) => setTimeout(resolve, 5000));
inferenceData = await getInference(inference.id);
}
// now the inference data is ready
console.log(inferenceData);
// add the function here
await registerIp(inferenceData);
}
main();
```
## 12. Done!
View the completed code for this tutorial.
Explore more tutorials in our documentation
# How to Register IP on Story
Source: https://docs.story.foundation/developers/tutorials/how-to-register-ip-on-story
Learn how to register an NFT as IP with proper metadata on Story.
Learn how to register an IP using the SDK.
Learn how to register an IP using the Smart Contracts.
# How to Register Music on Story
Source: https://docs.story.foundation/developers/tutorials/how-to-register-music-on-story
Learn how to properly register music on Story as an IP Asset using the Typescript SDK.
In this tutorial, you will learn how to properly register music as IP on Story using the TypeScript SDK. At the end, you will be able to listen to your song directly on our explorer.
View an example result after following this tutorial.
"Peaches" by Justin Bieber is one of the first RWAs coming to Story. Check out the announcement!
## 1. Create a Song
Before we register music on Story, you'll obviously need some music! If you already have music, make sure you have a link to the music file directly. For example, `https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3`. If you don't already have this, you can upload your music file to IPFS:
If you want to create a test song, go to [Suno](https://suno.com), which is an awesome platform for AI-generated music. We can get a test song by:
1. Inputting a prompt to create a song
2. Click on the final result, which should take you to a URL like `https://suno.com/song/dcd3076f-3aa5-400b-ba5d-87d30f27c311`
3. Copy the the `SONG_ID` in the URL (`dcd3076f-3aa5-400b-ba5d-87d30f27c311`)
4. Copy the following URL: `https://cdn1.suno.ai/${SONG_ID}.mp3`, making sure to replace `SONG_ID` with your own.
This is the URL we'll use in step 2.
## 2. Complete the "How to Register IP" Tutorial
Most of what we need to do is already covered in [Register an IP Asset](/developers/typescript-sdk/register-ip-asset). Complete that tutorial first, and then come back here.
## 3. Change Metadata
The only difference is how you set your metadata. Here is an example:
* `image.*` is used to display a cover image when your song is registered
* `media.*` is used for the audio file. Also note that the fields passed into `media.*` are checked for infringement by the [Story Attestation Service](/concepts/story-attestation-service).
```typescript main.ts theme={null}
const ipMetadata = {
title: "Midnight Marriage",
description: "This is a house-style song generated on suno.",
createdAt: "1740005219",
creators: [
{
name: "Jacob Tucker",
address: "0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112",
contributionPercent: 100,
},
],
image:
"https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg",
imageHash:
"0xc404730cdcdf7e5e54e8f16bc6687f97c6578a296f4a21b452d8a6ecabd61bcc",
mediaUrl: "https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3",
mediaHash:
"0xb52a44f53b2485ba772bd4857a443e1fb942cf5dda73c870e2d2238ecd607aee",
mediaType: "audio/mpeg",
};
```
After you've done that, you can set your NFT metadata like so:
* `image` for the cover image
* `animation_url` is used for the audio file
* `attributes` for any extra attributes you want to include
```typescript main.ts theme={null}
const nftMetadata = {
name: "Midnight Marriage",
description:
"This is a house-style song generated on suno. This NFT represents ownership of the IP Asset.",
image:
"https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg",
animation_url:
"https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3",
attributes: [
{
key: "Suno Artist",
value: "amazedneurofunk956",
},
{
key: "Artist ID",
value: "4123743b-8ba6-4028-a965-75b79a3ad424",
},
{
key: "Source",
value: "Suno.com",
},
],
};
```
## 4. Done!
When you run the script, you will register an IP Asset and it will look something like [this](https://aeneid.explorer.story.foundation/ipa/0x70920EaC7F9748Ac5A71C82310f1ac1C7eD11f02) on our explorer.
You can see the explorer recognizes the metadata format, and you can play the song directly on the page!
Explore more tutorials in our documentation
# How to Tip an IP
Source: https://docs.story.foundation/developers/tutorials/how-to-tip-an-ip
Learn how to tip an IP Asset using the SDK and Smart Contracts.
* [Use the SDK](#using-the-sdk)
* [Use a Smart Contract](#using-a-smart-contract)
# Using the SDK
See a completed, working example of setting up a simple derivative chain and
then tipping the child IP Asset.
In this tutorial, you will learn how to send money ("tip") an IP Asset using the TypeScript SDK.
## The Explanation
In this scenario, let's say there is a parent IP Asset that represents Mickey Mouse. Someone else draws a hat on that Mickey Mouse and registers it as a derivative (or "child") IP Asset. The License Terms specify that the child must share 50% of all commercial revenue (`commercialRevShare = 50`) with the parent. Someone else (a 3rd party user) comes along and wants to send the derivative 2 \$WIP for being really cool.
For the purposes of this example, we will assume the child is already registered as a derivative IP Asset. If you want help learning this, check out [Register a Derivative](/developers/typescript-sdk/register-derivative).
* Parent IP ID: `0x42595dA29B541770D9F9f298a014bF912655E183`
* Child IP ID: `0xeaa4Eed346373805B377F5a4fe1daeFeFB3D182a`
## 0. Before you Start
There are a few steps you have to complete before you can start the tutorial.
1. You will need to install [Node.js](https://nodejs.org/en/download) and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). If you've coded before, you likely have these.
2. Add your Story Network Testnet wallet's private key to `.env` file:
```text env theme={null}
WALLET_PRIVATE_KEY=
```
3. Add your preferred RPC URL to your `.env` file. You can just use the public default one we provide:
```text env theme={null}
RPC_PROVIDER_URL=https://aeneid.storyrpc.io
```
4. Install the dependencies:
```bash Terminal theme={null}
npm install @story-protocol/core-sdk viem
```
## 1. Set up your Story Config
In a `utils.ts` file, add the following code to set up your Story Config:
* Associated docs: [TypeScript SDK Setup](/developers/typescript-sdk/setup)
```typescript utils.ts theme={null}
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
import { http } from "viem";
import { privateKeyToAccount, Address, Account } from "viem/accounts";
const privateKey: Address = `0x${process.env.WALLET_PRIVATE_KEY}`;
export const account: Account = privateKeyToAccount(privateKey);
const config: StoryConfig = {
account: account,
transport: http(process.env.RPC_PROVIDER_URL),
chainId: "aeneid",
};
export const client = StoryClient.newClient(config);
```
## 2. Tipping the Derivative IP Asset
Now create a `main.ts` file. We will use the `payRoyaltyOnBehalf` function to pay the derivative asset.
You will be paying the IP Asset with [\$WIP](https://aeneid.storyscan.io/address/0x1514000000000000000000000000000000000000). **Note that if you don't have enough \$WIP, the function will auto wrap an equivalent amount of \$IP into \$WIP for you.** If you don't have enough of either, it will fail.
**Whitelisted Revenue Tokens**
Only tokens that are whitelisted by our protocol can be used as payment ("revenue") tokens. \$WIP is one of those tokens. To see that list, go [here](/developers/deployed-smart-contracts).
Now we can call the `payRoyaltyOnBehalf` function. In this case:
1. `receiverIpId` is the `ipId` of the derivative (child) asset
2. `payerIpId` is `zeroAddress` because the payer is a 3rd party (someone that thinks Mickey Mouse with a hat on him is cool), and not necessarily another IP Asset
3. `token` is the address of \$WIP, which can be found [here](/concepts/royalty-module/ip-royalty-vault#whitelisted-revenue-tokens)
4. `amount` is 2, since the person tipping wants to send 2 \$WIP
```typescript main.ts theme={null}
import { client } from "./utils";
import { zeroAddress, parseEther } from "viem";
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
async function main() {
const response = await client.royalty.payRoyaltyOnBehalf({
receiverIpId: "0xeaa4Eed346373805B377F5a4fe1daeFeFB3D182a",
payerIpId: zeroAddress,
token: WIP_TOKEN_ADDRESS,
amount: parseEther("2"), // 2 $WIP
});
console.log(`Paid royalty at transaction hash ${response.txHash}`);
}
main();
```
## 3. Child Claiming Due Revenue
At this point we have already finished the tutorial: we learned how to tip an IP Asset. But what if the child and parent want to claim their due revenue?
The child has been paid 2 \$WIP. But remember, it shares 50% of its revenue with the parent IP because of the `commercialRevenue = 50` in the license terms.
The child IP can claim its 1 \$WIP by calling the `claimAllRevenue` function:
* `ancestorIpId` is the `ipId` we're claiming revenue for
* `currencyTokens` is an array that contains the address of \$WIP, which can be found [here](/concepts/royalty-module/ip-royalty-vault#whitelisted-revenue-tokens)
* `claimer` is the address that holds the [royalty tokens](/concepts/royalty-module/ip-royalty-vault#royalty-tokens) associated with the child's [IP Royalty Vault](/concepts/royalty-module/ip-royalty-vault). By default, they are in the IP Account, which is just the `ipId` of the child asset.
Claiming revenue is permissionless. Any wallet can run the claim revenue transaction for an IP.
```typescript main.ts theme={null}
import { client } from "./utils";
import { zeroAddress, parseEther } from "viem";
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
async function main() {
// previous code here ...
const response = await client.royalty.claimAllRevenue({
ancestorIpId: "0xDa03c4B278AD44f5a669e9b73580F91AeDE0E3B2",
claimer: "0xDa03c4B278AD44f5a669e9b73580F91AeDE0E3B2",
currencyTokens: [WIP_TOKEN_ADDRESS],
childIpIds: [],
royaltyPolicies: [],
});
console.log(`Claimed revenue: ${response.claimedTokens}`);
}
main();
```
## 4. Parent Claiming Due Revenue
Continuing, the parent should be able to claim its revenue as well. In this example, the parent should be able to claim 1 \$WIP since the child earned 2 \$WIP and the `commercialRevShare = 50` in the license terms.
We will use the `claimAllRevenue` function to claim the due revenue tokens.
1. `ancestorIpId` is the `ipId` of the parent ("ancestor") asset
2. `claimer` is the address that holds the royalty tokens associated with the parent's [IP Royalty Vault](/concepts/royalty-module/ip-royalty-vault). By default, they are in the IP Account, which is just the `ipId` of the parent asset
3. `childIpIds` will have the `ipId` of the child asset
4. `royaltyPolicies` will contain the address of the royalty policy. As explained in [Royalty Module](/concepts/royalty-module), this is either `RoyaltyPolicyLAP` or `RoyaltyPolicyLRP`, depending on the license terms. In this case, let's assume the license terms specify a `RoyaltyPolicyLAP`. Simply go to [Deployed Smart Contracts](/developers/deployed-smart-contracts) and find the correct address.
5. `currencyTokens` is an array that contains the address of \$WIP, which can be found [here](/concepts/royalty-module/ip-royalty-vault#whitelisted-revenue-tokens)
Claiming revenue is permissionless. Any wallet can run the claim revenue transaction for an IP.
```typescript main.ts theme={null}
import { client } from "./utils";
import { zeroAddress, parseEther } from "viem";
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
async function main() {
// previous code here ...
const response = await client.royalty.claimAllRevenue({
ancestorIpId: "0x089d75C9b7E441dA3115AF93FF9A855BDdbfe384",
claimer: "0x089d75C9b7E441dA3115AF93FF9A855BDdbfe384",
currencyTokens: [WIP_TOKEN_ADDRESS],
childIpIds: ["0xDa03c4B278AD44f5a669e9b73580F91AeDE0E3B2"],
royaltyPolicies: ["0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E"],
});
console.log(`Claimed revenue: ${response.claimedTokens}`);
}
main();
```
## 5. Where does revenue get claimed to? Why do I not see it in my wallet? Why is it in the IP Account?
A couple of things to note here:
1. By default, revenue is claimed to the IP Account, not the IP owner's
wallet. This is because when an IP is created, the royalty tokens for that IP
(where revenue gets claimed to) are minted to the IP Account.
2. To transfer revenue from IP Account -> IP Owner's wallet, an IP owner can
run the `transferErc20` function (it will not work
if you run it for someone else's IP obviously).
```typescript main.ts theme={null}
// Docs: https://docs.story.foundation/sdk-reference/ipaccount#transfererc20
const transferFromIpAccount = await client.ipAccount.transferErc20({
ipId: "0xDa03c4B278AD44f5a669e9b73580F91AeDE0E3B2",
tokens: [
{
address: WIP_TOKEN_ADDRESS,
amount: parseEther("1"),
target: "0x02", // the external wallet
},
],
});
```
3. If the **IP owner** (the wallet that owns the underlying IP ownership NFT) had
called `claimAllRevenue` before, our SDK would automatically transfer the revenue
to the IP owner's wallet for convenience.
4. If the IP owner wants revenue to always be claimed to their wallet and
avoid the middle step, they should transfer the royalty tokens from the IP
account to their wallet.
```typescript main.ts theme={null}
// Get the IP Royalty Vault Address
// Note: This is equivalent to the currency address of the ERC-20
// Royalty Tokens for this IP.
const royaltyVaultAddress = await client.royalty.getRoyaltyVaultAddress(
"0xDa03c4B278AD44f5a669e9b73580F91AeDE0E3B2" as Address
);
// Transfer the Royalty Tokens from the IP Account to the address
// executing this transaction (you could use any other address as well)
const transferRoyaltyTokens = await client.ipAccount.transferErc20({
ipId: "0xDa03c4B278AD44f5a669e9b73580F91AeDE0E3B2" as Address,
tokens: [
{
address: royaltyVaultAddress,
amount: 100 * 1_000_000, // 100% of the royalty tokens
target: "0x02", // the external wallet
},
],
});
```
## 6. Done!
See a completed, working example of setting up a simple derivative chain and
then tipping the child IP Asset.
# Using a Smart Contract
View the tutorial here!
# Overview
Source: https://docs.story.foundation/developers/tutorials/overview
A list of follow-along developer tutorials.
## Registration
* [How to Register IP on Story](/developers/tutorials/how-to-register-ip-on-story)
* [How to Register Music on Story](/developers/tutorials/how-to-register-music-on-story)
* [Case Study: Registering a Derivative of Ippy](/developers/tutorials/case-study-register-derivative-ippy)
## Royalty
* [How to Tip an IP](/developers/tutorials/how-to-tip-an-ip)
## AI-Themed
* [Protect DALL·E AI-Generated Images](/developers/tutorials/protect-dalle-ai-generated-images)
* [Register & Monetize Stability Images](/developers/tutorials/register-stability-images)
* [Finetune Images on Story](/developers/tutorials/finetune-images)
## Wallet-less / Onboarding
* [Email Login & Sponsored Transactions with Privy](/developers/tutorials/privy-tutorial)
# Email Login & Sponsored Transactions with Privy
Source: https://docs.story.foundation/developers/tutorials/privy-tutorial
Learn how to implement email logins and sponsored transactions with Privy & Pimlico.
View the completed code for this tutorial.
You are reading this tutorial because you probably want to do one or both of these things:
1. Enable users who don't have a wallet to login with email to your app ("Embedded Wallets")
2. Sponsor transactions for your users so they don't have to pay gas ("Smart Wallets")
Here is how Privy describes both of these things:
> Embedded wallets are self-custodial wallets provisioned by Privy itself for a wallet experience that is directly embedded in your application. Embedded wallets do not require a separate wallet client, like a browser extension or a mobile app, and can be accessed directly from your product. These are primarily designed for users of your app who may not already have an external wallet, or don't want to connect their external wallet.
>
> Smart wallets are programmable, onchain accounts that incorporate the features of account abstraction. With just a few lines of code, you can create smart wallets for your users to sponsor gas payments, send batched transactions, and more.
We will be implementing both using [Privy](https://www.privy.io/) + [Pimlico](https://www.pimlico.io/).
### ⚠️ Prerequisites
There are a few steps you have to complete before you can start the tutorial.
1. Create a new project on [Privy's Dashboard](https://dashboard.privy.io)
2. Copy your **"App ID"** under **"App settings > API keys"**. In your local project, make a `.env` file and add your App ID:
```Text .env theme={null}
NEXT_PUBLIC_PRIVY_APP_ID=
```
3. On your project dashboard, enable Smart Wallets under "**Wallet Configuration > Smart wallets**" and select "**Kernel (ZeroDev)**" as shown below:
4. Once you enable Smart wallets, right underneath make sure to put a "Custom chain" with the following values:
1. Name: `Story Aeneid Testnet`
2. ID number: `1315`
3. RPC URL: `https://aeneid.storyrpc.io`
4. For the Bundler URL and Paymaster URL, go to [Pimlico's Dashboard](https://dashboard.pimlico.io) and create a new app. Then click on "API Keys", create a new API Key, click "RPC URLs" as shown below, and then select "Story Aeneid Testnet" as the network:
This is for testing. In a real scenario, you would have to set up proper sponsorship policies and billing info on Pimlico to automatically sponsor the transactions on behalf of your app. We don't have to do this on testnet.
5. Install the dependencies:
```Text Terminal theme={null}
npm install @story-protocol/core-sdk permissionless viem @privy-io/react-auth
```
## 1. Set up Embedded Wallets
Follow Privy's official tutorial for setup instead of reading this step.
This part of the Privy documentation
[here](https://docs.privy.io/basics/react/advanced/automatic-wallet-creation#automatic-wallet-creation)
describes setting up Embedded Wallets automatically, which is a fancy way of
saying it supports email login, such that when a user logs in with email it
creates a wallet for them. In the below example, we simply create an embedded
wallet for every user, but you may want more customization by reading their
tutorial.
You must wrap any component that will be using embedded/smart wallets with the `PrivyProvider` and `SmartWalletsProvider`. In a `providers.tsx` (or whatever you want to call it) file, add the following code:
```jsx providers.tsx theme={null}
"use client";
import { PrivyProvider } from "@privy-io/react-auth";
import { SmartWalletsProvider } from "@privy-io/react-auth/smart-wallets";
import { aeneid } from "@story-protocol/core-sdk";
export default function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Then you can simply add it to your`layout.tsx` like so:
```jsx layout.tsx theme={null}
import Providers from "@/providers/providers";
/* other code here... */
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode,
}>) {
return (
{children}
);
}
```
## 2. Login & Logout
You can add email login to your app like so:
```jsx page.tsx theme={null}
import { usePrivy } from "@privy-io/react-auth";
export default function Home() {
const { login, logout, user } = usePrivy();
useEffect(() => {
if (user) {
const smartWallet = user.linkedAccounts.find(
(account) => account.type === "smart_wallet"
);
// Logs the smart wallet's address
console.log(smartWallet.address);
// Logs the smart wallet type (e.g. 'safe', 'kernel', 'light_account', 'biconomy', 'thirdweb', 'coinbase_smart_wallet')
console.log(smartWallet.type);
}
}, [user]);
return (
);
}
```
## 3. Sign a Message with Privy
Follow Privy's official tutorial for signing messages instead of reading
this step.
We can use the generated smart wallet to sign messages:
```jsx page.tsx theme={null}
import { useSmartWallets } from "@privy-io/react-auth/smart-wallets";
export default function Home() {
const { client: smartWalletClient } = useSmartWallets();
/* previous code here */
async function sign() {
const uiOptions = {
title: "Example Sign",
description: "This is an example for a user to sign.",
buttonText: "Sign",
};
const request = {
message: "IP is cool",
};
const signature = await smartWalletClient?.signMessage(request, {
uiOptions,
});
}
return (
{/* previous code here */}
);
}
```
## 4. Send an Arbitrary Transaction
Follow Privy's official tutorial for sending transactions instead of reading
this step.
We can also use the generated smart wallet to sponsor transactions for our users:
```jsx page.tsx theme={null}
import { useSmartWallets } from "@privy-io/react-auth/smart-wallets";
import { encodeFunctionData } from "viem";
import { defaultNftContractAbi } from "./defaultNftContractAbi";
export default function Home() {
const { client: smartWalletClient } = useSmartWallets();
/* previous code here */
async function mintNFT() {
const uiOptions = {
title: "Mint NFT",
description: "This is an example transaction that mints an NFT.",
buttonText: "Mint",
};
const transactionRequest = {
to: "0x937bef10ba6fb941ed84b8d249abc76031429a9a", // example nft contract
data: encodeFunctionData({
abi: defaultNftContractAbi, // abi from another file
functionName: "mintNFT",
args: ["0x6B86B39F03558A8a4E9252d73F2bDeBfBedf5b68", "test-uri"],
}),
} as const;
const txHash = await smartWalletClient?.sendTransaction(
transactionRequest,
{ uiOptions }
);
console.log(`View Tx: https://aeneid.storyscan.io/tx/${txHash}`);
}
return (
{/* previous code here */}
)
}
```
```Text defaultNftContractAbi.ts theme={null}
export const defaultNftContractAbi = [
{
inputs: [],
stateMutability: "nonpayable",
type: "constructor",
},
{
inputs: [
{
internalType: "address",
name: "recipient",
type: "address",
},
{
internalType: "string",
name: "tokenURI",
type: "string",
},
],
name: "mintNFT",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "uint256",
name: "tokenId",
type: "uint256",
},
],
name: "tokenURI",
outputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "uint256",
name: "tokenId",
type: "uint256",
},
],
name: "ownerOf",
outputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "symbol",
outputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "name",
outputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "totalSupply",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
];
```
## 5. Send a Transaction from Story SDK
We can also use the generated smart wallet to send transactions from the [🛠️ TypeScript SDK](/developers/typescript-sdk). Some of the functions have an option to return the `encodedTxData`, which we can use to pass into Privy's smart wallet. You can see which functions support this in the [SDK Reference](/sdk-reference).
```jsx page.tsx theme={null}
import { useSmartWallets } from "@privy-io/react-auth/smart-wallets";
import {
EncodedTxData,
StoryClient,
StoryConfig,
} from "@story-protocol/core-sdk";
import { http } from "viem";
export default function Home() {
const { client: smartWalletClient } = useSmartWallets();
/* previous code here */
async function setupStoryClient() {
const config: StoryConfig = {
account: smartWalletClient!.account,
transport: http("https://aeneid.storyrpc.io"),
chainId: "aeneid",
};
const client = StoryClient.newClient(config);
return client;
}
async function registerIp() {
const storyClient = await setupStoryClient();
const response = await storyClient.ipAsset.registerIpAsset({
nft: {
type: "mint",
spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc",
},
});
const uiOptions = {
title: "Register IP",
description: "This is an example transaction that registers an IP.",
buttonText: "Register",
};
const txHash = await smartWalletClient?.sendTransaction(
response.encodedTxData as EncodedTxData,
{ uiOptions }
);
console.log(`View Tx: https://aeneid.storyscan.io/tx/${txHash}`);
}
return (
{/* previous code here */}
);
}
```
## 6. Done!
View the completed code for this tutorial.
Explore more tutorials in our documentation
# Protect DALL·E AI-Generated Images
Source: https://docs.story.foundation/developers/tutorials/protect-dalle-ai-generated-images
Learn how to license and protect DALL·E AI-Generated images on Story.
In this tutorial, you will learn how to license and protect DALL·E 2 AI-Generated images by registering it on Story.
## The Explanation
Let's say you generate an image using AI. Without adding a proper license to your image, others could use it freely. In this tutorial, you will learn how to add a license to your DALL·E 2 AI-Generated image so that if others want to use it, they must properly license it from you.
In order to register that IP on Story, you first need to mint an NFT to represent that IP, and then register that NFT on Story, turning it into an [IP Asset](/concepts/ip-asset).
## 0. Before you Start
There are a few steps you have to complete before you can start the tutorial.
1. You will need to install [Node.js](https://nodejs.org/en/download) and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). If you've coded before, you likely have these.
2. Add your Story Network Testnet wallet's private key to `.env` file:
```yaml .env theme={null}
WALLET_PRIVATE_KEY=
```
3. Go to [Pinata](https://pinata.cloud/) and create a new API key. Add the JWT to your `.env` file:
```yaml .env theme={null}
PINATA_JWT=
```
4. Go to [OpenAI](https://platform.openai.com/settings/organization/api-keys) and create a new API key. Add the new key to your `.env` file:
**OpenAI Credits**
In order to generate an image, you'll need OpenAI credits. If you just created an account, you will probably have a free trial that will give you a few credits to start with.
```yaml .env theme={null}
OPENAI_API_KEY=
```
5. Add your preferred RPC URL to your `.env` file. You can just use the public default one we provide:
```yaml .env theme={null}
RPC_PROVIDER_URL=https://aeneid.storyrpc.io
```
6. Install the dependencies:
```bash Terminal theme={null}
npm install @story-protocol/core-sdk pinata-web3 viem openai
```
## 1. Generate an Image
In a `main.ts` file, add the following code to generate an image:
```typescript main.ts theme={null}
import OpenAI from "openai";
async function main() {
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const image = await openai.images.generate({
model: "dall-e-2",
prompt: "A cute baby sea otter",
});
console.log(image.data[0].url); // the url to the newly created image
}
main();
```
## 2. Set up your Story Config
In a `utils.ts` file, add the following code to set up your Story Config:
Associated docs: [TypeScript SDK Setup](/developers/typescript-sdk/setup)
```typescript utils.ts theme={null}
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
import { http } from "viem";
import { privateKeyToAccount, Address, Account } from "viem/accounts";
const privateKey: Address = `0x${process.env.WALLET_PRIVATE_KEY}`;
export const account: Account = privateKeyToAccount(privateKey);
const config: StoryConfig = {
account: account,
transport: http(process.env.RPC_PROVIDER_URL),
chainId: "aeneid",
};
export const client = StoryClient.newClient(config);
```
## 3. Set up your IP Metadata
View the [IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard) and construct your metadata for your IP. Back in the `main.ts` file, properly format your metadata as shown below:
```typescript main.ts theme={null}
import OpenAI from "openai";
import { client, account } from "./utils";
async function main() {
// previous code here ...
const ipMetadata = {
title: "Baby Sea Otter",
description: "A baby sea otter generated with DALL·E.",
createdAt: "1728401700",
image: image.data[0].url,
imageHash: "0x...", // a hash of the image
mediaUrl: image.data[0].url,
mediaHash: "0x...", // a hash of the image
mediaType: "image/png",
creators: [
{
name: "Jacob Tucker",
address: "0x67ee74EE04A0E6d14Ca6C27428B27F3EFd5CD084",
description: "A cool dev rel person.",
contributionPercent: 100,
socialMedia: [
{
platform: "Twitter",
url: "https://x.com/jacobmtucker",
},
],
},
],
};
}
main();
```
## 4. Set up your NFT Metadata
The NFT Metadata follows the [ERC-721 Metadata Standard](https://eips.ethereum.org/EIPS/eip-721).
```typescript main.ts theme={null}
import OpenAI from "openai";
import { client, account } from "./utils";
async function main() {
// previous code here ...
const nftMetadata = {
name: "Image Ownership NFT",
description:
"This NFT represents ownership of the image generated by Dall-E 2",
image: image.data[0].url,
attributes: [
{
key: "Model",
value: "dall-e-2",
},
{
key: "Prompt",
value: "A cute baby sea otter",
},
],
};
}
main();
```
## 5. Upload & Register IP
Now that we have all of our metadata set up, you can easily complete this tutorial by going to [Register an IP Asset](/developers/typescript-sdk/register-ip-asset#3-%5Boptional%5D-upload-your-ip-and-nft-metadata-to-ipfs) and **completing steps 3 (Upload your IP and NFT Metadata to IPFS) & 4 (Register an NFT as an IP Asset)**.
Once you have done that, you should see a console log with a link to our IP-explorer that shows your registered AI generated image.
## 6. Done!
Explore more tutorials in our documentation
# Register & Monetize Stability Images
Source: https://docs.story.foundation/developers/tutorials/register-stability-images
Learn how to register, protect, and monetize Stability AI-Generated images on Story.
In this tutorial, you will learn how to:
1. Generate an image with Stability AI
2. Upload your image to Pinata IPFS
3. Register your image as IP on Story
4. Attach License Terms to your IP
## The Explanation
Let's say you generate an image using Stability AI. Without adding a proper license to your image, others could use it freely. In this tutorial, you will learn how to add a license to your Stability AI-Generated image so that if others want to use it, they must properly license it from you.
## 0. Before you Start
There are a few steps you have to complete before you can start the tutorial.
1. You will need to install [Node.js](https://nodejs.org/en/download) and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). If you've coded before, you likely have these.
2. Add your Story Network Testnet wallet's private key to `.env` file:
```yaml .env theme={null}
WALLET_PRIVATE_KEY=
```
3. Go to [the Pinata dashboard](https://app.pinata.cloud/developers/api-keys) and create a new API key and a gateway. Add the JWT along with the gateway to your `.env` file:
```yaml .env theme={null}
PINATA_JWT=
PINATA_GATEWAY=
```
4. Go to [Stability](https://platform.stability.ai/account/keys) and create a new API key. Add the new key to your `.env` file:
**Stability Credits**
In order to generate an image, you'll need Stability credits. If you just created an account, you will probably have a free trial that will give you a few credits to start with.
```yaml .env theme={null}
STABILITY_API_KEY=
```
5. Add your preferred RPC URL to your `.env` file. You can just use the public default one we provide:
```yaml .env theme={null}
RPC_PROVIDER_URL=https://aeneid.storyrpc.io
```
6. Install the dependencies:
```bash Terminal theme={null}
npm install @story-protocol/core-sdk pinata-web3 viem axios sharp form-data
```
## 1. Generate an Image
You can follow the [Stability API Reference](https://platform.stability.ai/docs/api-reference) to use the model of your choice. For this tutorial, we'll be using Stability's **Stable Image Core** generate endpoint to generate an image. The below is taken directly from their documentation.
Create a `main.ts` file and add the following code:
```typescript main.ts theme={null}
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
async function main() {
const payload = {
prompt: "Lighthouse on a cliff overlooking the ocean",
output_format: "png",
};
const response = await axios.postForm(
`https://api.stability.ai/v2beta/stable-image/generate/core`,
axios.toFormData(payload, new FormData()),
{
validateStatus: undefined,
responseType: "arraybuffer",
headers: {
Authorization: `Bearer ${process.env.STABILITY_API_KEY}`,
Accept: "image/*",
},
}
);
}
main();
```
## 1.5. (Optional) Condense the Image
Stability generates images that are heavy in size, and therefore expensive to store. Optionally, we can condense the produced image for faster loading speeds and less expensive storage costs.
```typescript main.ts theme={null}
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
async function main() {
// previous code here ...
const condensedImgBuffer = await sharp(response.data)
.png({ quality: 10 }) // Adjust the quality value as needed (between 0 and 100)
.toBuffer();
}
main();
```
## 2. Store Image in IPFS
Now that we have our image, we need to store it on IPFS so we can get a URL back to access it. In this tutorial we'll be using [Pinata](https://pinata.cloud/), a decentralized storage solution that makes storing images easy.
In a separate file `uploadToIpfs.ts`, create a function `uploadBlobToIPFS` that uploads our buffer to IPFS:
```typescript uploadToIpfs.ts theme={null}
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
// you can put your pinata gateway here, or leave it empty
pinataGateway: process.env.PINATA_GATEWAY,
});
// upload our image to ipfs by putting it in a public group
export async function uploadBlobToIPFS(
blob: Blob,
fileName: string
): Promise {
const file = new File([blob], fileName, { type: "image/png" });
const { IpfsHash } = await pinata.upload.file(file);
return IpfsHash;
}
```
Back in the main file, call the `uploadBlobToIPFS` function to store our image:
```typescript main.ts theme={null}
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS } from "./uploadToIpfs.ts";
async function main() {
// previous code here ...
// convert the buffer to a blob
const blob = new Blob([condensedImgBuffer], { type: "image/png" });
// store the blob on ipfs
const imageCid = await uploadBlobToIPFS(blob, "lighthouse.png");
}
main();
```
## 3. Set up your Story Config
Now that we have generated and stored our image, we can register the image as IP on Story. First, let's set up our config. In a `utils.ts` file, add the following code:
Associated docs: [TypeScript SDK Setup](/developers/typescript-sdk/setup)
```typescript utils.ts theme={null}
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
import { http } from "viem";
import { privateKeyToAccount, Address, Account } from "viem/accounts";
const privateKey: Address = `0x${process.env.WALLET_PRIVATE_KEY}`;
export const account: Account = privateKeyToAccount(privateKey);
const config: StoryConfig = {
account: account,
transport: http(process.env.RPC_PROVIDER_URL),
chainId: "aeneid",
};
export const client = StoryClient.newClient(config);
```
## 4. Set up your IP Metadata
View the [IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard) and construct the metadata for your IP. Properly format your metadata as shown below:
```typescript main.ts theme={null}
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS } from "./uploadToIpfs.ts";
import { client, account } from "./utils";
async function main() {
// previous code here ...
const ipMetadata = {
title: "Lighthouse",
description: "A generated picture of a lighthouse.",
createdAt: "1728401700",
image: process.env.PINATA_GATEWAY + "/files/" + imageCid,
imageHash: "0x...", // a hash of the image
mediaUrl: process.env.PINATA_GATEWAY + "/files/" + imageCid,
mediaHash: "0x...", // a hash of the image
mediaType: "image/png",
creators: [
{
name: "Jacob Tucker",
address: "0x67ee74EE04A0E6d14Ca6C27428B27F3EFd5CD084",
description: "A cool dev rel person.",
contributionPercent: 100,
socialMedia: [
{
platform: "Twitter",
url: "https://x.com/jacobmtucker",
},
],
},
],
};
}
main();
```
## 5. Set up your NFT Metadata
The NFT Metadata follows the [ERC-721 Metadata Standard](https://eips.ethereum.org/EIPS/eip-721).
```typescript main.ts theme={null}
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS } from "./uploadToIpfs.ts";
import { client, account } from "./utils";
async function main() {
// previous code here ...
const nftMetadata = {
name: "Ownership NFT",
description:
"This NFT represents ownership of the image generated by Stability",
image: process.env.PINATA_GATEWAY + "/files/" + imageCid,
attributes: [
{
key: "Model",
value: "Stability",
},
{
key: "Service",
value: "Stable Image Core",
},
{
key: "Prompt",
value: "Lighthouse on a cliff overlooking the ocean",
},
],
};
}
main();
```
## 6. Upload your IP and NFT Metadata to IPFS
In the `uploadToIpfs.ts` file, create a function to upload your IP & NFT Metadata objects to IPFS:
```typescript uploadToIpfs.ts theme={null}
// previous code here ...
export async function uploadJSONToIPFS(jsonMetadata: any): Promise {
const { IpfsHash } = await pinata.upload.json(jsonMetadata);
return IpfsHash;
}
```
You can then use that function to upload your metadata, as shown below:
```typescript main.ts theme={null}
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS, uploadJSONToIPFS } from "./uploadToIpfs.ts";
import { client, account } from "./utils";
import { createHash } from "crypto";
async function main() {
// previous code here ...
const ipIpfsHash = await uploadJSONToIPFS(ipMetadata);
const ipHash = createHash("sha256")
.update(JSON.stringify(ipMetadata))
.digest("hex");
const nftIpfsHash = await uploadJSONToIPFS(nftMetadata);
const nftHash = createHash("sha256")
.update(JSON.stringify(nftMetadata))
.digest("hex");
}
main();
```
## 7. Create License Terms
When registering your image on Story, you can attach [License Terms](/concepts/licensing-module/license-terms) to the IP. These are real, legally binding terms enforced on-chain by the [Licensing Module](/concepts/licensing-module), disputable by the [Dispute Module](/concepts/dispute-module), and in the worst case, able to be enforced off-chain in court through traditional means.
Let's say we want to monetize our image such that every time someone wants to use it (on merch, advertisement, or whatever) they have to pay an initial minting fee of 10 \$WIP. Additionally, every time they earn revenue on derivative work, they owe 5% revenue back as royalty.
```typescript main.ts theme={null}
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS, uploadJSONToIPFS } from "./uploadToIpfs.ts";
import { client, account } from "./utils";
import { createHash } from "crypto";
import { LicenseTerms, WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
import { zeroAddress, parseEther } from "viem";
async function main() {
// previous code here ...
const commercialRemixTerms: LicenseTerms = {
transferable: true,
royaltyPolicy: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", // RoyaltyPolicyLAP address from https://docs.story.foundation/developers/deployed-smart-contracts
defaultMintingFee: parseEther("1"), // 1 $WIP
expiration: BigInt(0),
commercialUse: true,
commercialAttribution: true, // must give us attribution
commercializerChecker: zeroAddress,
commercializerCheckerData: zeroAddress,
commercialRevShare: 5, // can claim 50% of derivative revenue
commercialRevCeiling: BigInt(0),
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: BigInt(0),
currency: WIP_TOKEN_ADDRESS,
uri: "",
};
}
main();
```
## 8. Register an NFT as an IP Asset
Next we will mint an NFT, register it as an [IP Asset](/concepts/ip-asset), set [License Terms](/concepts/licensing-module/license-terms) on the IP, and then set both NFT & IP metadata.
Luckily, we can use the `registerIpAsset` function to mint an NFT and register it as an IP Asset in the same transaction.
This function needs an SPG NFT Contract to mint from. For simplicity, you can use a public collection we have created for you on Aeneid testnet: `0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc`.
Using the public collection we provide for you is fine, but when you do this for real, you should make your own NFT Collection for your IPs. You can do this in 2 ways:
1. Deploy a contract that implements the [ISPGNFT](https://github.com/storyprotocol/protocol-periphery-v1/blob/main/contracts/interfaces/ISPGNFT.sol) interface, or use the SDK's [createNFTCollection](/sdk-reference/nftclient#createnftcollection) function (shown below) to do it for you. This will give you your own SPG NFT Collection that only you can mint from.
```typescript createSpgNftCollection.ts theme={null}
import { zeroAddress } from "viem";
import { client } from "./utils";
async function createSpgNftCollection() {
const newCollection = await client.nftClient.createNFTCollection({
name: "Test NFTs",
symbol: "TEST",
isPublicMinting: false,
mintOpen: true,
mintFeeRecipient: zeroAddress,
contractURI: "",
});
console.log(
`New SPG NFT collection created at transaction hash ${newCollection.txHash}`
);
console.log(`NFT contract address: ${newCollection.spgNftContract}`);
}
createSpgNftCollection();
```
2. Create a custom ERC-721 NFT collection on your own. See a working code example [here](https://github.com/storyprotocol/typescript-tutorial/blob/main/scripts/registration/registerCustom.ts). This is helpful if you **already have a custom NFT contract that has your own custom logic, or if your IPs themselves are NFTs.**
Associated Docs:
[ipAsset.registerIpAsset](/sdk-reference/ipasset#registeripasset)
```typescript main.ts theme={null}
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS, uploadJSONToIPFS } from "./uploadToIpfs.ts";
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
import { client, account } from "./utils";
import { createHash } from "crypto";
import { LicenseTerms } from "@story-protocol/core-sdk";
import { zeroAddress, parseEther, Address } from "viem";
async function main() {
// previous code here ...
const response = await client.ipAsset.registerIpAsset({
nft: {
type: "mint",
spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc",
},
// the terms we created in the previous step
licenseTermsData: [{ terms: commercialRemixTerms }],
ipMetadata: {
ipMetadataURI: process.env.PINATA_GATEWAY + "/files/" + ipIpfsHash,
ipMetadataHash: `0x${ipHash}`,
nftMetadataURI: process.env.PINATA_GATEWAY + "/files/" + nftIpfsHash,
nftMetadataHash: `0x${nftHash}`,
},
});
console.log(
`Root IPA created at transaction hash ${response.txHash}, IPA ID: ${response.ipId}`
);
console.log(
`View on the explorer: https://aeneid.explorer.story.foundation/ipa/${response.ipId}`
);
}
main();
```
## 9. Done!
Congratulations! Now your image is registered on Story with commercial license terms.
Explore more tutorials in our documentation
# Consumer
Source: https://docs.story.foundation/sdk-reference/cdr/consumer
Methods for requesting and performing CDR decryption.
## Consumer
The `Consumer` sub-client handles read requests, partial decryption collection from validators, and final decryption. Requires a `walletClient`.
```typescript theme={null}
const consumer = client.consumer;
```
The SDK also exposes `readVault` as an alias for `accessCDR`, and
`readFileVault` as an alias for `downloadFile`.
Partial decryptions are collected from the Story-API REST endpoint
(`/dkg/cdr_partials`), keyed by `(uuid, requesterPubKey)`. The keeper verifies
each validator's signature on ingress, so the SDK does not re-verify signatures
locally. For defense-in-depth, pass an `attestationConfig` to verify each
validator's SGX enclave before accepting their partials.
### Methods
* accessCDR
* downloadFile
* read
* collectPartials
* decryptDataKey
* prefetchRegistry
***
### accessCDR
High-level method that submits a read request, collects partial decryptions from validators, and combines them to recover the original data.
| Method | Type |
| ----------- | --------------------------------------------------------- |
| `accessCDR` | `(params: AccessCDRParams) => Promise` |
Parameters:
* `params.uuid`: `number` - The vault UUID
* `params.accessAuxData`: `` `0x${string}` `` - Auxiliary data passed to the read condition
* `params.requesterPubKey` *(optional)*: `` `0x${string}` `` - Uncompressed secp256k1 public key (65 bytes, `0x04` prefix). If omitted, the SDK generates an ephemeral keypair.
* `params.recipientPrivKey` *(optional)*: `Uint8Array` - 32-byte secp256k1 private key (for ECIES decryption of partials). If omitted, the SDK generates an ephemeral keypair and zeroes it after use.
* `params.globalPubKey` *(optional)*: `Uint8Array` - DKG global public key (from `observer.getGlobalPubKey()`). If omitted, the SDK queries it for you.
* `params.timeoutMs` *(optional)*: `number` - Timeout for collecting partials. `120_000` is a good starting point.
* `params.feeOverride` *(optional)*: `bigint` - Explicit read fee. Skips the `readFee()` auto-query (strict-equality semantics — not a way to pay a different amount).
* `params.onInvalidPartial` *(optional)*: `(event, error) => void` - Called when a validator's partial is excluded because its attestation failed the `attestationConfig` checks.
* `params.attestationConfig` *(optional)*: `AttestationConfig` - Verifies each validator's SGX enclave before accepting their partials.
```typescript Example theme={null}
const { dataKey, txHash } = await client.consumer.accessCDR({
uuid: 42,
accessAuxData: "0x",
timeoutMs: 120_000,
});
const secret = new TextDecoder().decode(dataKey);
console.log(`Read tx: ${txHash}`);
console.log(`Decrypted: ${secret}`);
```
If you want the shortest high-level path, you can omit `requesterPubKey`,
`recipientPrivKey`, and `globalPubKey` and let `accessCDR()` fill them in.
The threshold is derived automatically from the partial-decryption bucket's
DKG round.
Throws `EmptyVaultError` synchronously if the vault has never been written to.
This is raised by a preflight chain read *before* the fee-bearing `read()`
transaction is submitted, so no fee is spent.
```typescript AccessCDRResponse theme={null}
interface AccessCDRResponse {
dataKey: Uint8Array; // the recovered plaintext
txHash: `0x${string}`; // read request transaction hash
}
```
***
### downloadFile
High-level method that reads the encrypted file key through CDR, downloads the
encrypted blob from a `StorageProvider`, and returns the decrypted file bytes.
Parameters:
* `params.uuid`: `number` - The vault UUID
* `params.accessAuxData`: `` `0x${string}` `` - Auxiliary data passed to the read condition
* `params.storageProvider`: `StorageProvider` - Backend used to fetch the encrypted content
* `params.requesterPubKey` *(optional)*: `` `0x${string}` `` - Explicit requester public key for the read flow
* `params.recipientPrivKey` *(optional)*: `Uint8Array` - Explicit recipient private key for the read flow
* `params.globalPubKey` *(optional)*: `Uint8Array` - DKG global public key. Auto-queried if omitted.
* `params.timeoutMs` *(optional)*: `number` - Timeout for validator partial collection
* `params.feeOverride` *(optional)*: `bigint` - Explicit read fee. Skips the `readFee()` auto-query.
* `params.onInvalidPartial` *(optional)*: `(event, error) => void` - Called when a validator's partial is excluded because its attestation failed the `attestationConfig` checks.
* `params.attestationConfig` *(optional)*: `AttestationConfig` - Verifies each validator's SGX enclave before accepting their partials.
* `params.skipCidVerification` *(optional)*: `boolean` - Skip CID integrity verification of the downloaded encrypted file (default: `false`)
```typescript Example theme={null}
import { writeFile } from "node:fs/promises";
const { content } = await client.consumer.downloadFile({
uuid: 42,
accessAuxData: "0x",
storageProvider,
timeoutMs: 120_000,
});
await writeFile("./example.decrypted.pdf", Buffer.from(content));
console.log("Saved ./example.decrypted.pdf");
```
For Story license-gated reads, `accessAuxData` should encode the caller's
license token IDs as `abi.encode(uint256[] licenseTokenIds)`.
`downloadFile()` inherits the same optional key auto-management behavior as
`accessCDR()`, and returns `cid` and `txHash` alongside `content`.
`content` is the raw decrypted file bytes. Decode it as text only if the
original file was text-based.
***
### read
Submits a read request on-chain. The caller must satisfy the vault's read condition. This prompts validators to submit encrypted partial decryptions.
| Method | Type |
| ------ | ----------------------------------------------- |
| `read` | `(params: ReadParams) => Promise` |
Parameters:
* `params.uuid`: `number` - The vault UUID
* `params.accessAuxData`: `` `0x${string}` `` - Auxiliary data passed to the read condition
* `params.requesterPubKey`: `` `0x${string}` `` - Your ephemeral uncompressed secp256k1 public key. Partials are indexed by this value.
* `params.feeOverride` *(optional)*: `bigint` - Explicit read fee. Skips the `readFee()` auto-query.
```typescript Example theme={null}
const { txHash } = await client.consumer.read({
uuid: 42,
accessAuxData: "0x",
requesterPubKey,
});
```
```typescript ReadResponse theme={null}
interface ReadResponse {
txHash: `0x${string}`;
}
```
***
### collectPartials
Polls the Story-API REST endpoint (`/dkg/cdr_partials`) until at least a
threshold's worth of partial-decryption submissions have been surfaced for the
given `(uuid, requesterPubKey)`.
| Method | Type |
| ----------------- | ---------------------------------------------------------------------- |
| `collectPartials` | `(params: CollectPartialsParams) => Promise` |
Parameters:
* `params.uuid`: `number` - The vault UUID
* `params.requesterPubKey`: `` `0x${string}` `` - The uncompressed secp256k1 public key used in the matching `read()` request
* `params.timeoutMs` *(optional)*: `number` - Timeout in milliseconds. `120_000` is a good starting point.
* `params.pollIntervalMs` *(optional)*: `number` - Polling interval in milliseconds
* `params.onInvalidPartial` *(optional)*: `(event, error) => void` - Called when a validator's partial is excluded because its attestation failed the `attestationConfig` checks
* `params.attestationConfig` *(optional)*: `AttestationConfig` - Verifies each validator's SGX enclave and excludes partials from untrusted validators
The required threshold is derived from the partial-decryption bucket's own DKG
round (`observer.getThresholdAt(round)`), so a DKG rollover mid-poll does not
measure the bucket against the wrong round. The vault ciphertext is read once at
the start of the call and pinned for the rest of the poll loop.
Throws `PartialCollectionTimeoutError` if the timeout is reached before enough
partials are collected. Throws `EmptyVaultError` if the vault has never been
written to.
```typescript Example theme={null}
const partials = await client.consumer.collectPartials({
uuid: 42,
requesterPubKey,
timeoutMs: 120_000,
});
console.log(`Collected ${partials.length} partials`);
```
```typescript PartialDecryptionEvent theme={null}
interface PartialDecryptionEvent {
validator: `0x${string}`;
round: number;
pid: number; // 1-based participant index
encryptedPartial: `0x${string}`; // AES-GCM encrypted
ephemeralPubKey: `0x${string}`; // 65 bytes, uncompressed secp256k1
pubShare: `0x${string}`; // 34 bytes, Ed25519 with curve-code prefix
uuid: number;
ciphertext: `0x${string}`; // TDH2 ciphertext this partial decrypts
}
```
Every event returned from a successful `collectPartials` call shares the same
`round` and `ciphertext` — the result is filtered to the bucket matching the
vault's current ciphertext.
***
### decryptDataKey
Decrypts the collected partial decryptions using ECIES, then combines them via TDH2 to recover the original plaintext.
| Method | Type |
| ---------------- | ------------------------------------------------ |
| `decryptDataKey` | `(params: DecryptParams) => Promise` |
Parameters:
* `params.ciphertext`: `TDH2Ciphertext` - The encrypted data (`{ raw, label }`)
* `params.partials`: `PartialDecryptionEvent[]` - Collected partial decryptions
* `params.recipientPrivKey`: `Uint8Array` - Your ephemeral secp256k1 private key (32 bytes)
* `params.globalPubKey`: `Uint8Array` - DKG global public key
* `params.label`: `Uint8Array` - 32-byte label (from `uuidToLabel(uuid)`)
The TDH2 combine threshold is taken implicitly as `partials.length` —
`collectPartials` already returns exactly the threshold count needed for
reconstruction. Pass exactly the partials you want combined.
Throws `InsufficientPartialsError` if fewer partials are passed than the
ciphertext requires. Throws `InvalidCiphertextError` if the ciphertext is
empty or malformed.
```typescript Example theme={null}
import { uuidToLabel } from "@piplabs/cdr-sdk";
const label = uuidToLabel(uuid);
const dataKey = await client.consumer.decryptDataKey({
ciphertext: { raw: ciphertextBytes, label },
partials,
recipientPrivKey,
globalPubKey,
label,
});
const secret = new TextDecoder().decode(dataKey);
```
***
### prefetchRegistry
Warms the validator `commPubKey` + attestation cache for the active DKG round.
The first `accessCDR()` / `downloadFile()` call after construction would
otherwise stall on this fetch. Frontends that know a read is imminent (for
example, right after wallet connection) can call this in the background.
| Method | Type |
| ------------------ | --------------------- |
| `prefetchRegistry` | `() => Promise` |
```typescript Example theme={null}
// Best-effort warm-up — safe to call repeatedly
client.consumer.prefetchRegistry().catch(() => {});
```
# Crypto Utilities
Source: https://docs.story.foundation/sdk-reference/cdr/crypto
Low-level TDH2 and ECIES cryptographic primitives used by the CDR SDK.
## Crypto Utilities
These are the low-level cryptographic functions used internally by the CDR SDK. You typically won't need to call these directly unless you're building custom decryption flows.
All crypto functions require WASM to be initialized first via `initWasm()`.
The current Aeneid release also includes SGX attestation verification
utilities, including `verifyAttestation()`, for checking validator
attestations against expected enclave measurements and security version
values.
### Functions
* initWasm
* tdh2Encrypt
* tdh2Verify
* tdh2Combine
* decryptPartial
* parseSgxQuote
* verifyAttestation
* uuidToLabel
***
### initWasm
Initializes the WebAssembly module required for all TDH2 cryptographic operations. Must be called once before any other crypto function.
| Function | Type |
| ---------- | --------------------- |
| `initWasm` | `() => Promise` |
```typescript Example theme={null}
import { initWasm } from "@piplabs/cdr-sdk";
await initWasm();
// Now TDH2 functions are ready to use
```
Throws `WasmNotInitializedError` if you call any TDH2 function before `initWasm()` completes.
***
### tdh2Encrypt
Encrypts plaintext using TDH2 threshold encryption against the DKG global public key.
| Function | Type |
| ------------- | -------------------------------------------------------- |
| `tdh2Encrypt` | `(params: TDH2EncryptParams) => Promise` |
Parameters:
* `params.plaintext`: `Uint8Array` - The data to encrypt
* `params.globalPubKey`: `Uint8Array` - DKG global public key (34 bytes with Ed25519 prefix)
* `params.label`: `Uint8Array` - 32-byte label binding ciphertext to a specific vault
```typescript Example theme={null}
import { tdh2Encrypt, uuidToLabel } from "@piplabs/cdr-sdk";
const ciphertext = await tdh2Encrypt({
plaintext: new TextEncoder().encode("secret"),
globalPubKey,
label: uuidToLabel(42),
});
```
```typescript TDH2Ciphertext theme={null}
interface TDH2Ciphertext {
raw: Uint8Array; // serialized ciphertext (cb-mpc format)
label: Uint8Array; // 32-byte context binding
}
```
***
### tdh2Verify
Validates a TDH2 ciphertext without decrypting. Useful for checking integrity.
| Function | Type |
| ------------ | ------------------------------------------------ |
| `tdh2Verify` | `(params: TDH2VerifyParams) => Promise` |
Parameters:
* `params.ciphertext`: `Uint8Array` - The raw ciphertext bytes
* `params.globalPubKey`: `Uint8Array` - DKG global public key
* `params.label`: `Uint8Array` - The 32-byte label used during encryption
```typescript Example theme={null}
import { tdh2Verify } from "@piplabs/cdr-sdk";
const isValid = await tdh2Verify({
ciphertext: ciphertext.raw,
globalPubKey,
label: uuidToLabel(42),
});
console.log(`Ciphertext valid: ${isValid}`);
```
***
### tdh2Combine
Combines threshold partial decryptions to recover the original plaintext.
| Function | Type |
| ------------- | ---------------------------------------------------- |
| `tdh2Combine` | `(params: TDH2CombineParams) => Promise` |
Parameters:
* `params.ciphertext`: `TDH2Ciphertext` - The encrypted data (`{ raw, label }`)
* `params.partials`: `DecryptedPartial[]` - Array of decrypted partials (must have `>= threshold`)
* `params.globalPubKey`: `Uint8Array` - DKG global public key
* `params.label`: `Uint8Array` - 32-byte label
* `params.threshold`: `number` - Minimum number of partials required
```typescript Example theme={null}
import { tdh2Combine } from "@piplabs/cdr-sdk";
const plaintext = await tdh2Combine({
ciphertext,
partials: decryptedPartials,
globalPubKey,
label: uuidToLabel(42),
threshold: 2,
});
```
```typescript DecryptedPartial theme={null}
interface DecryptedPartial {
name: string; // validator name — key in the access structure
pubShare: Uint8Array; // 34 bytes (Ed25519 with 0x043f prefix)
partial: Uint8Array; // raw decrypted partial bytes
}
```
***
### decryptPartial
Decrypts a single encrypted partial decryption from a validator using ECIES (ECDH + HKDF + AES-256-GCM).
| Function | Type |
| ---------------- | ------------------------------------------------------- |
| `decryptPartial` | `(params: DecryptPartialParams) => Promise` |
Parameters:
* `params.encryptedPartial`: `Uint8Array` - The encrypted partial (nonce || ciphertext || tag)
* `params.ephemeralPubKey`: `Uint8Array` - Validator's ephemeral public key (65 bytes, uncompressed secp256k1)
* `params.recipientPrivKey`: `Uint8Array` - Your ephemeral private key (32 bytes, secp256k1)
```typescript Example theme={null}
import { decryptPartial } from "@piplabs/cdr-sdk";
const rawPartial = await decryptPartial({
encryptedPartial: partialBytes,
ephemeralPubKey: validatorEphemeralPubKey,
recipientPrivKey: myEphemeralPrivKey,
});
```
**ECIES Protocol:**
1. ECDH: `sharedPoint = secp256k1(recipientPrivKey, ephemeralPubKey)`
2. HKDF-SHA256: `aesKey = hkdf(sha256, sharedSecret, info="dkg-tdh2-partial", 32)`
3. AES-256-GCM: decrypt with 12-byte nonce from the encrypted partial
***
### verifyAttestation
Verifies a validator SGX attestation document against the expected enclave
measurements and minimum security version checks. Use it with the data returned
by `observer.getValidatorAttestations()` when your application wants an
explicit `MRENCLAVE`, `MRSIGNER`, and `SVN` trust check.
Parameters:
* `report`: `Uint8Array` - Raw SGX DCAP Quote v3 bytes
* `config.expectedMrEnclave` *(optional)*: `` `0x${string}` `` - Expected enclave measurement
* `config.expectedMrSigner` *(optional)*: `` `0x${string}` `` - Expected signer measurement
* `config.minSecurityVersion` *(optional)*: `number` - Minimum allowed ISV SVN
```typescript Example theme={null}
import { verifyAttestation } from "@piplabs/cdr-sdk";
const result = await verifyAttestation(report, {
expectedMrEnclave: "0x51c08cf3...",
expectedMrSigner: "0xa6f9d44c...",
minSecurityVersion: 1,
});
if (!result.valid) {
console.error(result.error);
}
```
`verifyAttestation()` performs client-side field checks. The quote signature
chain itself is verified on-chain; this helper is an extra allowlist /
policy check for SDK consumers.
***
### parseSgxQuote
Parses the key fields from an SGX DCAP Quote v3 without applying any policy.
| Function | Type |
| --------------- | ------------------------------------------------------------------ |
| `parseSgxQuote` | `(report: Uint8Array) => { mrEnclave, mrSigner, securityVersion }` |
Parameters:
* `report`: `Uint8Array` - Raw SGX DCAP Quote v3 bytes
Returns:
* `mrEnclave`: `` `0x${string}` `` - Parsed enclave measurement
* `mrSigner`: `` `0x${string}` `` - Parsed signer measurement
* `securityVersion`: `number` - Parsed ISV SVN
```typescript Example theme={null}
import { parseSgxQuote } from "@piplabs/cdr-sdk";
const fields = parseSgxQuote(report);
console.log(fields.mrEnclave);
console.log(fields.mrSigner);
console.log(fields.securityVersion);
```
***
### uuidToLabel
Derives a deterministic 32-byte label from a vault UUID. Used to bind ciphertext to a specific vault.
| Function | Type |
| ------------- | ------------------------------ |
| `uuidToLabel` | `(uuid: number) => Uint8Array` |
Parameters:
* `uuid`: `number` - The vault UUID (uint32)
Returns a 32-byte `Uint8Array`: 28 zero bytes followed by the 4-byte big-endian UUID.
```typescript Example theme={null}
import { uuidToLabel } from "@piplabs/cdr-sdk";
const label = uuidToLabel(42);
// Uint8Array(32) [0, 0, ..., 0, 0, 0, 0, 42]
```
# Observer
Source: https://docs.story.foundation/sdk-reference/cdr/observer
Read-only methods for querying CDR vault data, fees, and DKG state.
## Observer
The `Observer` sub-client is always available, even without a `walletClient`. It provides read-only access to CDR and DKG state.
```typescript theme={null}
const observer = client.observer;
```
The Observer reads from two backends: CDR contract state (vaults, fees,
`maxEncryptedDataSize`, operational threshold) over the EVM `publicClient`, and
DKG state (active round, global public key, threshold, validators,
attestations) over the Story-API REST endpoint configured by `apiUrl`.
Round-keyed DKG snapshots are cached for the lifetime of the Observer for
rounds in the stable Active and Ended stages, with in-flight request
deduplication. `getActiveRound()` always re-fetches, since the active round
can transition at any time.
### Methods
* getVault
* getAllocateFee
* getWriteFee
* getReadFee
* getMaxEncryptedDataSize
* getOperationalThreshold
* getActiveRound
* getGlobalPubKey
* getParticipantCount
* getThreshold
* getThresholdAt
* getRegisteredValidators
* getValidatorAttestations
***
### getVault
Fetch a vault's on-chain data by UUID.
| Method | Type |
| ---------- | ---------------------------------- |
| `getVault` | `(uuid: number) => Promise` |
Parameters:
* `uuid`: The vault's unique identifier (uint32)
```typescript Example theme={null}
const vault = await client.observer.getVault(42);
console.log(vault.uuid); // 42
console.log(vault.updatable); // false
console.log(vault.writeConditionAddr); // "0x..."
console.log(vault.readConditionAddr); // "0x..."
console.log(vault.encryptedData); // "0x..." (hex-encoded TDH2 ciphertext)
```
```typescript Vault theme={null}
interface Vault {
uuid: number;
updatable: boolean;
writeConditionAddr: `0x${string}`;
readConditionAddr: `0x${string}`;
writeConditionData: `0x${string}`;
readConditionData: `0x${string}`;
encryptedData: `0x${string}`;
}
```
***
### getAllocateFee
Returns the current fee (in wei) required to allocate a new vault.
| Method | Type |
| ---------------- | ----------------------- |
| `getAllocateFee` | `() => Promise` |
```typescript Example theme={null}
const fee = await client.observer.getAllocateFee();
console.log(`Allocate fee: ${fee} wei`);
```
***
### getWriteFee
Returns the current fee (in wei) required to write data to a vault.
| Method | Type |
| ------------- | ----------------------- |
| `getWriteFee` | `() => Promise` |
```typescript Example theme={null}
const fee = await client.observer.getWriteFee();
console.log(`Write fee: ${fee} wei`);
```
***
### getReadFee
Returns the current fee (in wei) required to submit a read request.
| Method | Type |
| ------------ | ----------------------- |
| `getReadFee` | `() => Promise` |
```typescript Example theme={null}
const fee = await client.observer.getReadFee();
console.log(`Read fee: ${fee} wei`);
```
***
### getMaxEncryptedDataSize
Returns the maximum encrypted payload size (in bytes) supported by the on-chain
vault path. The CDR contract treats this as a constant, so it is cached for the
lifetime of the Observer.
| Method | Type |
| ------------------------- | ----------------------- |
| `getMaxEncryptedDataSize` | `() => Promise` |
```typescript Example theme={null}
const maxSize = await client.observer.getMaxEncryptedDataSize();
console.log(`Max encrypted payload size: ${maxSize} bytes`);
```
***
### getOperationalThreshold
Returns the DKG operational threshold as a basis-points constant read from the
DKG contract (e.g., `667` = 66.7%).
| Method | Type |
| ------------------------- | ----------------------- |
| `getOperationalThreshold` | `() => Promise` |
```typescript Example theme={null}
const threshold = await client.observer.getOperationalThreshold();
console.log(`Threshold: ${Number(threshold) / 10}%`); // e.g. "66.7%"
```
***
### getActiveRound
Returns the currently active DKG round number. Always hits the Story-API REST
endpoint — the active round can transition at any time, so it is never served
from cache.
| Method | Type |
| ---------------- | ----------------------- |
| `getActiveRound` | `() => Promise` |
```typescript Example theme={null}
const round = await client.observer.getActiveRound();
console.log(`Active DKG round: ${round}`);
```
***
### getGlobalPubKey
Returns the DKG global public key from the active round, used for TDH2
encryption. This is an Ed25519 point with a 2-byte curve-code prefix (`0x043f`),
34 bytes total, so it can be passed directly to the WASM TDH2 functions.
| Method | Type |
| ----------------- | --------------------------- |
| `getGlobalPubKey` | `() => Promise` |
```typescript Example theme={null}
const globalPubKey = await client.observer.getGlobalPubKey();
console.log(globalPubKey.length); // 34
```
***
### getParticipantCount
Returns the number of validators selected to participate in the active DKG round.
| Method | Type |
| --------------------- | ----------------------- |
| `getParticipantCount` | `() => Promise` |
```typescript Example theme={null}
const count = await client.observer.getParticipantCount();
console.log(`Participating validators: ${count}`);
```
***
### getThreshold
Returns the absolute threshold for the currently active DKG round — the minimum
number of partial decryptions needed to combine. If `minThresholdRatio` is set
on the client, the effective value is
`max(network.threshold, ceil(participants * minThresholdRatio))`.
| Method | Type |
| -------------- | ----------------------- |
| `getThreshold` | `() => Promise` |
```typescript Example theme={null}
const threshold = await client.observer.getThreshold();
console.log(`Need ${threshold} partials to decrypt`);
```
Use this for UI / status display. Do **not** use it to validate a specific
partial-decryption bucket from `collectPartials` — a bucket carries its own
round, which can differ from the active round during DKG rollover. Use
`getThresholdAt` for that.
***
### getThresholdAt
Returns the absolute threshold for a specific DKG round, computed against that
round's own participant total. Used internally by `collectPartials` so a DKG
rollover mid-poll does not measure a bucket against the wrong round.
| Method | Type |
| ---------------- | ------------------------------------ |
| `getThresholdAt` | `(round: number) => Promise` |
Parameters:
* `round`: The DKG round number to compute the threshold for
```typescript Example theme={null}
const threshold = await client.observer.getThresholdAt(4);
console.log(`Round 4 threshold: ${threshold}`);
```
***
### getRegisteredValidators
Returns a map of validator address → `commPubKey` bytes for the given DKG round
(defaults to the active round). Only includes validators with `Finalized`
status. The `commPubKey` is the secp256k1 public key the validator's TEE uses to
sign partial decryption responses.
| Method | Type |
| ------------------------- | ------------------------------------------------------------------- |
| `getRegisteredValidators` | `(params?: { round?: number }) => Promise