Skip to main content

Create Atom

[Problem Internal Link] are the fundamental building blocks in the Intuition system, representing discrete units of data that can be anything from a single concept to a complex entity. Each Atom is assigned a unique decentralized identifier (DID) and includes associated metadata, a wallet, and a vault.

Context

The createAtom and batchCreateAtom functions are part of the EthMultiVault contract, which manages the creation and management of Atoms and their associated vaults. When creating Atoms, the metadata is stored off-chain with only a URI reference stored on-chain. This design allows for maximum flexibility - developers can use any URI scheme and storage solution that best fits their needs.

For convenience, we provide a set of GraphQL mutations that help structure and publish Atom metadata following common schemas and best practices. These mutations will be continuously updated to support more schemas and improve interoperability. See our Off-Chain Metadata Guide for details on using these recommended patterns.

The following guide will focus solely on the on-chain aspects of creating an Atom using a URI.

Step 1: Pin Content to IPFS

You may have noticed that the createAtom methods takes in a byte array called atomUri:

  function createAtom(bytes atomUri) payable returns (uint256),

This URI can be anything, but the most common approach is to use an IPFS CID which points to a JSON blob of metadata. Our API has rich support for schema.org/Thing schema, so we will provide an example of that. You can pin the content yourself, using an IPFS package like Pinata, or by connecting directly to an IPFS node such as a self hosted instance of Kubo... Or you can just call a mutation endpoint in our GraphQL API.

Complete example shown below.

Example: Pin content using our GraphQL API

const API_URL = '<https://dev.base-sepolia.intuition-api.com/v1/graphql>'

async function pinMetadata(thing: { name: string; description: string; image?: string; url?: string }) {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
mutation IntuitionPinMetadata($thing: PinThingInput!) {
pinThing(thing: $thing) {
uri
}
}
`,
variables: { thing },
}),
})

const result = await response.json()
return result.data.pinThing.uri
}

// Usage
const uri = await pinMetadata({
name: 'Cat',
description: 'Feline creature. Takes naps. Has cheeseburger.',
image: 'www.cats.com/picture_of_a_cat.jpeg',
url: 'www.cats.com',
})

console.log('IPFS URI:', uri)

Step 2: Get Creation Cost

There is a minimum atom creation cost in our protocol. To retrieve the currently configured value, just call getAtomCost() as shown below. It returns a uint256 in wei, which you will need to use as the eth value in your smart contract call. You can also use more than this, if you want to increase the initial deposit when creating the atom. The atom creation value goes into the pro-rata vault of the atom.

const atomCost = await publicClient.readContract({
address: config.multivaultAddress,
abi: MULTIVAULT_ABI,
functionName: 'getAtomCost',
})

Step 3: Check if Atom Already Exists (Optional)

You can check for an exact metadata match of your atom by directly reading the atomsByHash table in the smart contract. This is a cryptographically secure way of preventing duplicate data, although the smart contract iself will revert if you try to create an atom by providing a URI that already has an atom associated with it.

const atomHash = keccak256(stringToHex(ipfsCid))
const existingAtomId = await publicClient.readContract({
address: config.multivaultAddress,
abi: MULTIVAULT_ABI,
functionName: 'atomsByHash',
args: [atomHash],
})

if (existingAtomId !== 0n) {
// Atom already exists, use existingAtomId
return existingAtomId
}

You could also check for this using our GraphQL API, like this:

query AlreadyUsingURI {
atoms(where: { data: { _eq: "ipfs://bafkreigzazsddlnynfx2sw7tlwoqrvd5yebx2kmdhaqica2z7mugcow3nq" } }) {
term_id
value {
thing {
description
image
name
url
}
}
}
}

The query above checks the Intuition API to see if there is an atom with the provided URI, then returns the Term ID along with the Metadata if something is found.

If you wanted to perform a fuzzy search to see if there's a similar atom to the one you want to create, you could submit a query similar to this one:

query SearchAtoms($searchStr: String!) {
atoms(
where: {
_or: [
{ value: { thing: { name: { _ilike: $searchStr } } } }
{ value: { thing: { description: { _ilike: $searchStr } } } }
]
}
limit: 5
order_by: { term: { total_market_cap: desc } }
) {
term_id
value {
thing {
name
description
image
url
}
}
term {
total_market_cap
}
}
}

Step 4: Create the Atom

Once you're sure you want to create your atom, you just need to call createAtom and pass in the URI to the metadata. You might want your end user to do this, which would require them to confirm the transaction using their Ethereum wallet, and would place the burden of funding the transaction on them. It would also make them the recipient of any shares minted by the ETH value used in the transaction for the Atom. While Deposits and Redeems in existing atoms can specify any recipient, Atom Creation always uses the caller as the recipient.

Alternatively, you may want to create the atom on your server, in your backend, without prompting any user or minting the initial shares to them. This is useful in cases when you are using Intuition to manage data for your app that doesn't strictly belong to users -- such as creating a tag for your app to filter queries by in the future, or creating data which you need to filter by creator. For example, you might want to filter for atoms or triples that were created by your application server, and not by users.

Consider the following triple: [User X][Has Tag] [Verified]

You might be creating triples like this once a user has verified their account with your application. But you only want to treat these triples as valid if they were created by the wallet controlled by your backend. Otherwise, someone else could create this triple without verifying the user via your app, and it would show up as a false positive.

createAtom

function createAtom(bytes calldata atomUri) external payable returns (uint256)

Parameters

  • atomUri : The URI point to the Atom’s metadata (typically stored off-chain)
  • value : Initial deposit into the Atom’s multivault. Must be ≥ to the atom creation cost.
  • Returns: uint256 - Created atom vault ID

Implementation

// useCreateAtom Hook
import { type GetContractReturnType } from 'viem'
import { base } from 'viem/chains'
import { useContractWriteAndWait } from './useContractWriteAndWait'
import { useMultivaultContract } from './useMultivaultContract'

export const useCreateAtom = () => {
const multivault = useMultivaultContract(
baseSepolia.id
) as GetContractReturnType

return useContractWriteAndWait({
...multivault,
functionName: 'createAtom',
})
}
// Usage Example
const {
writeAsync: writeCreateAtom,
awaitingWalletConfirmation,
awaitingOnChainConfirmation,
} = useCreateAtom()

async function handleCreateAtom(atomData: string) {
if (!awaitingOnChainConfirmation && !awaitingWalletConfirmation && writeCreateAtom) {
try {
const tx = await writeCreateAtom({
address: MULTIVAULT_CONTRACT_ADDRESS,
abi: multivaultAbi,
functionName: 'createAtom',
args: [toHex(atomData)],
value: atomCost, // Must be >= minimum creation cost
})

if (tx?.hash) {
const receipt = await publicClient.waitForTransactionReceipt({
hash: tx.hash,
})
// Handle success
}
} catch (error) {
// Handle error
}
}
}

batchCreateAtom

If you need to create multiple atoms in a single transaction, you can use batchCreateAtom. This method takes an array of URIs and creates all atoms atomically. The cost is the atom creation cost multiplied by the number of atoms you're creating. Since these are all performed in a single EVM transaction, if any of them revert, none of the atoms will be created. Here is a back-end example:

If you need to create multiple atoms in a single transaction, you can use batchCreateAtom. This method takes an array of URIs and creates all atoms atomically. The cost is the atom creation cost multiplied by the number of atoms you're creating. Since these are all performed in a single EVM transaction, if any of them revert, none of the atoms will be created. Here is a back-end example:

function createAtomBatch(bytes[] calldata atomUris) external payable nonReentrant whenNotPaused returns (uint256[] memory)

Parameters

  • atomUris : The URIs point to the Atoms’ metadata (typically stored off-chain)
  • value : Initial deposit into the Atom’s multivault. Must be ≥ to the atom creation cost * length of atomUris. This will get distributed evenly across all created atoms.
  • Returns uint256[] - Created atoms’ vault ID(s)

Implementation

// useBatchCreateAtom Hook
import { type GetContractReturnType } from 'viem'
import { base } from 'viem/chains'
import { useContractWriteAndWait } from './useContractWriteAndWait'
import { useMultivaultContract } from './useMultivaultContract'

export const useBatchCreateAtom = () => {
const multivault = useMultivaultContract(
base.id
) as GetContractReturnType

return useContractWriteAndWait({
...multivault,
functionName: 'createAtomBatch',
})
}
// Usage Example
const {
writeAsync: writeBatchCreateAtom,
awaitingWalletConfirmation,
awaitingOnChainConfirmation,
} = useBatchCreateAtom()

async function handleBatchCreateAtom(atomUris: string[]) {
const value = BigInt(atomCost) * BigInt(atomUris.length)

if (!awaitingOnChainConfirmation && !awaitingWalletConfirmation && writeBatchCreateAtom) {
try {
const tx = await writeBatchCreateAtom({
address: MULTIVAULT_CONTRACT_ADDRESS,
abi: multivaultAbi,
functionName: 'createAtomBatch',
args: [atomUris.map(data => toHex(data))],
value: value, // Must be >= minimum creation cost * number of atoms
})

if (tx?.hash) {
const receipt = await publicClient.waitForTransactionReceipt({
hash: tx.hash,
})
// Handle success
}
} catch (error) {
// Handle error
}
}
}

Batch Creation Addendum:

If you are creating very large batches of atoms or triples (50-100 or more) in a single transaction, the smart contract may revert the transaction due to the block limit being exceeded. In cases like this, you will need to chunk the atoms or triples into smaller batches -- and then execute multiple batch transactions. You could simulate the batch transaction to see if it fits in the block, then chunk it down if the simulation reverts -- or you could just hard-code a limit based on your application. The limit does vary depending on the byte length of the parameters. For example, shorter URIs like keywords (follows, likes, etc) take up much less space in the stack than longer URIs like IPFS CIDs (ipfs://bafy4gyudijfdofjuioppszz883834jkskjfjkl;fjeidfdofkkofokd, etc)

Extracting Atom ID from Transaction

You may want the resulting Term ID once the Atom has been created. You can extract this information from the transaction hash / receipt like this:

export function extractAtomIdFromReceipt(receipt: TransactionReceipt): bigint {
const events = parseEventLogs({
logs: receipt.logs,
abi: [
{
type: 'event',
name: 'AtomCreated',
inputs: [
{ type: 'address', name: 'creator', indexed: true },
{ type: 'address', name: 'atomWallet', indexed: true },
{ type: 'bytes', name: 'atomData' },
{ type: 'uint256', name: 'vaultID' },
],
},
],
eventName: 'AtomCreated',
})

if (events.length === 0) {
throw new Error('No AtomCreated events found in receipt')
}

return (events[0].args as { vaultID: bigint }).vaultID
}

// Usage:
const receipt = await publicClient.waitForTransactionReceipt({ hash })
const atomId = extractAtomIdFromReceipt(receipt)

Cost Considerations

  1. Creation Cost:
    • Minimum ETH required to create an Atom
    • Retrieved via getAtomCost()
    • Includes protocol fee sent to treasury
    • Must be included in transaction's value parameter
  2. Initial Deposit:
    • Any ETH sent above the creation cost
    • Becomes initial stake in the Atom's vault
    • Subject to entry fees
    • Grants fractional ownership in the vault

See also: